From d90417129d8809a7f46aca9b11cb0b92c08e56f5 Mon Sep 17 00:00:00 2001 From: gmantele <gmantele@ari.uni-heidelberg.de> Date: Fri, 13 Nov 2015 18:50:50 +0100 Subject: [PATCH] [TAP & UWS] 2 MAJOR BUGS FIXED (these bugs were affecting performances). 1) [TAP & UWS] ]MAJOR BUG FIX: The abortion of an SQL query is now correctly implemented. Before this fix, 2 mistakes prevented this clean abortion: a/ The thread was not cancelled because the SQL query execution was blocking the thread. Then the thread could not treat the interruption though it was flagged as interrupted. b/ The function UWSJob.isStopped() considered the job as stopped because the interrupted flag was set, even though the thread was still processing (and the database too). Because of that it returned true and the job phase was ABORTED though the thread was still running. NOW: a/ TAPJob calls the function Statement.cancel() (if supported) in order to cancel the SQL query execution properly inside the database. b/ The function UWSJob.isStopped() does not test any more the interrupted flag and returns true only if the thread is really stopped. IN BRIEF: It is now sure that a job in the phase ABORTED is really stopped (that's to say: thread stopped AND DB query execution stopped). 2) [TAP] BUG FIX: When the writing of a result is abnormaly interrupted for any reason, the file which was being written is deleted. --- src/tap/ADQLExecutor.java | 49 ++- src/tap/TAPJob.java | 27 +- src/tap/config/ConfigurableTAPFactory.java | 5 +- src/tap/data/ResultSetTableIterator.java | 241 ++++++++++++- src/tap/db/DBConnection.java | 29 +- src/tap/db/JDBCConnection.java | 394 ++++++++++++++++++--- src/tap/formatter/FITSFormat.java | 10 +- src/tap/formatter/VOTableFormat.java | 19 +- src/uws/job/UWSJob.java | 14 +- 9 files changed, 713 insertions(+), 75 deletions(-) diff --git a/src/tap/ADQLExecutor.java b/src/tap/ADQLExecutor.java index 27ec125..841e023 100644 --- a/src/tap/ADQLExecutor.java +++ b/src/tap/ADQLExecutor.java @@ -104,7 +104,7 @@ import adql.query.ADQLQuery; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (04/2015) + * @version 2.1 (11/2015) */ public class ADQLExecutor { @@ -224,9 +224,20 @@ public class ADQLExecutor { try{ return start(); }catch(IOException ioe){ - throw new UWSException(ioe); + if (thread.isInterrupted()) + return report; + else + throw new UWSException(ioe); }catch(TAPException te){ - throw new UWSException(te.getHttpErrorCode(), te); + if (thread.isInterrupted()) + return report; + else + throw new UWSException(te.getHttpErrorCode(), te); + }catch(UWSException ue){ + if (thread.isInterrupted()) + return report; + else + throw ue; } } @@ -249,6 +260,17 @@ public class ADQLExecutor { dbConn = service.getFactory().getConnection(jobID); } + /** + * Cancel the current SQL query execution or result set fetching if any is currently running. + * If no such process is on going, this function has no effect. + * + * @since 2.1 + */ + public final void cancelQuery(){ + if (dbConn != null && progression == ExecutionProgression.EXECUTING_ADQL) + dbConn.cancel(true); + } + /** * <p>Start the synchronous processing of the ADQL query.</p> * @@ -625,12 +647,14 @@ public class ADQLExecutor { } // CASE ASYNCHRONOUS: else{ + boolean completed = false; long start = -1, end = -1; + Result result = null; + JobThread jobThread = (JobThread)thread; try{ // Create a UWS Result object to store the result // (the result will be stored in a file and this object is the association between the job and the result file): - JobThread jobThread = (JobThread)thread; - Result result = jobThread.createResult(); + result = jobThread.createResult(); // Set the MIME type of the result format in the result description: result.setMimeType(formatter.getMimeType()); @@ -646,10 +670,25 @@ public class ADQLExecutor { // Add the result description and link in the job description: jobThread.publishResult(result); + completed = true; + logger.logTAP(LogLevel.INFO, report, "RESULT_WRITTEN", "Result formatted (in " + formatter.getMimeType() + " ; " + (report.nbRows < 0 ? "?" : report.nbRows) + " rows ; " + ((report.resultingColumns == null) ? "?" : report.resultingColumns.length) + " columns) in " + ((start <= 0 || end <= 0) ? "?" : (end - start)) + "ms!", null); }catch(IOException ioe){ + // Propagate the exception: throw new UWSException(UWSException.INTERNAL_SERVER_ERROR, ioe, "Impossible to write in the file into the result of the job " + report.jobID + " must be written!"); + }finally{ + if (!completed){ + // Delete the result file (it is either incomplete or incorrect ; + // it is then not reliable and is anyway not associated with the job and so could not be later deleted when the job will be): + if (result != null){ + try{ + service.getFileManager().deleteResult(result, jobThread.getJob()); + }catch(IOException ioe){ + logger.logTAP(LogLevel.ERROR, report, "WRITING_RESULT", "The result writting has failed and the produced partial result must be deleted, but this deletion also failed! (job: " + report.jobID + ")", ioe); + } + } + } } } } diff --git a/src/tap/TAPJob.java b/src/tap/TAPJob.java index aa89491..c2e2ea9 100644 --- a/src/tap/TAPJob.java +++ b/src/tap/TAPJob.java @@ -45,7 +45,7 @@ import uws.service.log.UWSLog.LogLevel; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (04/2015) + * @version 2.1 (11/2015) */ public class TAPJob extends UWSJob { private static final long serialVersionUID = 1L; @@ -341,6 +341,31 @@ public class TAPJob extends UWSJob { } } + /** @since 2.1 */ + @Override + protected void stop(){ + if (!isStopped()){ + synchronized(thread){ + stopping = true; + + // Interrupts the thread: + thread.interrupt(); + + // Cancel the query execution if any currently running: + ((AsyncThread)thread).executor.cancelQuery(); + + // Wait a little for its end: + if (waitForStop > 0){ + try{ + thread.join(waitForStop); + }catch(InterruptedException ie){ + getLogger().logJob(LogLevel.WARNING, this, "END", "Unexpected InterruptedException while waiting for the end of the execution of the job \"" + jobId + "\" (thread ID: " + thread.getId() + ")!", ie); + } + } + } + } + } + /** * This exception is thrown by a job execution when no database connection are available anymore. * diff --git a/src/tap/config/ConfigurableTAPFactory.java b/src/tap/config/ConfigurableTAPFactory.java index 02432f7..d42e262 100644 --- a/src/tap/config/ConfigurableTAPFactory.java +++ b/src/tap/config/ConfigurableTAPFactory.java @@ -74,7 +74,7 @@ import adql.translator.PostgreSQLTranslator; * </p> * * @author Grégory Mantelet (ARI) - * @version 2.0 (04/2015) + * @version 2.1 (11/2015) * @since 2.0 */ public final class ConfigurableTAPFactory extends AbstractTAPFactory { @@ -281,6 +281,9 @@ public final class ConfigurableTAPFactory extends AbstractTAPFactory { @Override public void freeConnection(DBConnection conn){ try{ + // Cancel any possible query that could be running: + conn.cancel(false); + // Close the connection (if a connection pool is used, the connection is not really closed but is freed and kept in the pool for further usage): ((JDBCConnection)conn).getInnerConnection().close(); }catch(SQLException se){ service.getLogger().error("Can not close properly the connection \"" + conn.getID() + "\"!", se); diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java index af4fdea..f5e7684 100644 --- a/src/tap/data/ResultSetTableIterator.java +++ b/src/tap/data/ResultSetTableIterator.java @@ -22,6 +22,7 @@ package tap.data; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Timestamp; import java.util.NoSuchElementException; @@ -42,11 +43,16 @@ import adql.translator.JDBCTranslator; * </i></p> * * @author Grégory Mantelet (ARI) - * @version 2.1 (10/2015) + * @version 2.1 (11/2015) * @since 2.0 */ public class ResultSetTableIterator implements TableIterator { + /** Statement associated with the ResultSet/Dataset to read. + * <i>MAY be NULL</i> + * @since 2.1 */ + private final Statement stmt; + /** ResultSet/Dataset to read. */ private final ResultSet data; @@ -90,10 +96,41 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. * * @see #convertType(int, String, String) - * @see #ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet) throws NullPointerException, DataReadException{ - this(dataSet, null, null, null); + this(null, dataSet, null, null, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. + * </p> + * + * @param dataSet Dataset over which this iterator must iterate. + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) + * + * @since 2.1 + */ + public ResultSetTableIterator(final Statement stmt, final ResultSet dataSet) throws NullPointerException, DataReadException{ + this(stmt, dataSet, null, null, null); } /** @@ -127,10 +164,49 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. * * @see #convertType(int, String, String) - * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{ - this(dataSet, null, dbms, null); + this(null, dataSet, null, dbms, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. + * </p> + * + * <p><i><b>Important</b>: + * The second parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) + * + * @since 2.1 + */ + public ResultSetTableIterator(final Statement stmt, final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{ + this(stmt, dataSet, null, dbms, null); } /** @@ -159,10 +235,44 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. * * @see #convertType(int, String, String) - * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator) throws NullPointerException, DataReadException{ - this(dataSet, translator, null, null); + this(null, dataSet, translator, null, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + * </p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) + * + * @since 2.1 + */ + public ResultSetTableIterator(final Statement stmt, final ResultSet dataSet, final JDBCTranslator translator) throws NullPointerException, DataReadException{ + this(stmt, dataSet, translator, null, null); } /** @@ -199,10 +309,52 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. * * @see #convertType(int, String, String) - * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms) throws NullPointerException, DataReadException{ - this(dataSet, translator, dbms, null); + this(null, dataSet, translator, dbms, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(int, String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + * </p> + * + * <p><i><b>Important</b>: + * The third parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * <b>This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) + * + * @since 2.1 + */ + public ResultSetTableIterator(final Statement stmt, final ResultSet dataSet, final JDBCTranslator translator, final String dbms) throws NullPointerException, DataReadException{ + this(stmt, dataSet, translator, dbms, null); } /** @@ -256,12 +408,74 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the metadata (columns count and types) can not be fetched. * * @see #convertType(int, String, String) + * @see #ResultSetTableIterator(Statement, ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ + this(null, dataSet, translator, dbms, resultMeta); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is reading first the given metadata (if any), + * and then, try to guess the datatype from the DBMS column datatype (using {@link #convertType(int, String, String)}). + * </p> + * + * <h3>Provided metadata</h3> + * + * <p>The third parameter of this constructor aims to provide the metadata expected for each column of the ResultSet.</p> + * + * <p> + * For that, it is expected that all these metadata are {@link TAPColumn} objects. Indeed, simple {@link DBColumn} + * instances do not have the type information. If just {@link DBColumn}s are provided, the ADQL name it provides will be kept + * but the type will be guessed from the type provide by the ResultSetMetadata. + * </p> + * + * <p><i>Note: + * If this parameter is incomplete (array length less than the column count returned by the ResultSet or some array items are NULL), + * column metadata will be associated in the same order as the ResultSet columns. Missing metadata will be built from the + * {@link ResultSetMetaData} and so the types will be guessed. + * </i></p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(int, String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + * </p> + * + * <p><i><b>Important</b>: + * The third parameter of this constructor is given as second parameter of {@link #convertType(int, String, String)}. + * <b>This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * @param resultMeta List of expected columns. <i>note: these metadata are expected to be really {@link TAPColumn} objects ; MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the metadata (columns count and types) can not be fetched. + * + * @see #convertType(int, String, String) + * + * @since 2.1 + */ + public ResultSetTableIterator(final Statement stmt, final ResultSet dataSet, final JDBCTranslator translator, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ // A dataset MUST BE provided: if (dataSet == null) throw new NullPointerException("Missing ResultSet object over which to iterate!"); + // Set the associated statement: + this.stmt = stmt; + // Keep a reference to the ResultSet: data = dataSet; @@ -296,10 +510,17 @@ public class ResultSetTableIterator implements TableIterator { @Override public void close() throws DataReadException{ + boolean rsClosed = false; try{ data.close(); + rsClosed = true; + if (stmt != null) + stmt.close(); }catch(SQLException se){ - throw new DataReadException("Can not close the iterated ResultSet!", se); + if (!rsClosed) + throw new DataReadException("Can not close the iterated ResultSet!", se); + else + throw new DataReadException("ResultSet successfully closed but impossible to closed the associated Statement!", se); } } diff --git a/src/tap/db/DBConnection.java b/src/tap/db/DBConnection.java index e836211..612954e 100644 --- a/src/tap/db/DBConnection.java +++ b/src/tap/db/DBConnection.java @@ -42,7 +42,7 @@ import adql.query.ADQLQuery; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (03/2015) + * @version 2.1 (11/2015) */ public interface DBConnection { @@ -268,4 +268,31 @@ public interface DBConnection { */ public void setFetchSize(final int size); + /** + * <p>Stop the execution of the current query.</p> + * + * <p> + * If asked. a rollback of the current transaction can also be performed + * after the cancellation (if successful) of the query. + * </p> + * + * <p> + * This function should <b>never</b> return any kind of exception. This is particularly important + * in the following cases: + * </p> + * <ul> + * <li>this function is not implemented</li> + * <li>the database driver or another API used to interact with a "database" does not support the cancel operation</li> + * <li>no query is currently running</li> + * <li>a rollback is not possible or failed</li> + * </ul> + * <p>However, if an exception occurs it may be directly logged at least as a WARNING.</p> + * + * @param rollback <code>true</code> to cancel the statement AND rollback the current connection transaction, + * <code>false</code> to just cancel the statement. + * + * @since 2.1 + */ + public void cancel(final boolean rollback); + } diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index f8dc32d..d8ed597 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -27,6 +27,7 @@ import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Timestamp; import java.text.ParseException; @@ -70,6 +71,29 @@ import adql.translator.TranslationException; * Then it has been really tested successfully with Postgres and SQLite. * </i></p> * + * + * <h3>Only one query executed at a time!</h3> + * + * <p> + * With a single instance of {@link JDBCConnection} it is possible to execute only one query (whatever the type: SELECT, UPDATE, DELETE, ...) + * at a time. This is indeed the simple way chosen with this implementation in order to allow the cancellation of any query by managing only + * one {@link Statement}. Indeed, only a {@link Statement} has a cancel function able to stop any query execution on the database. + * So all queries are executed with the same {@link Statement}. Thus, allowing the execution of one query at a time lets + * abort only one query rather than several in once (though just one should have been stopped). + * </p> + * + * <p> + * All the following functions are synchronized in order to prevent parallel execution of them by several threads: + * {@link #addUploadedTable(TAPTable, TableIterator)}, {@link #dropUploadedTable(TAPTable)}, {@link #executeQuery(ADQLQuery)}, + * {@link #getTAPSchema()} and {@link #setTAPSchema(TAPMetadata)}. + * </p> + * + * <p> + * To cancel a query execution the function {@link #cancel(boolean)} must be called. No error is returned by this function in case + * no query is currently executing. + * </p> + * + * * <h3>Deal with different DBMS features</h3> * * <p>Update queries are taking into account whether the following features are supported by the DBMS:</p> @@ -99,6 +123,7 @@ import adql.translator.TranslationException; * All these features have no impact at all on ADQL query executions ({@link #executeQuery(ADQLQuery)}). * </i></p> * + * * <h3>Datatypes</h3> * * <p> @@ -119,6 +144,7 @@ import adql.translator.TranslationException; * and managed. * </p> * + * * <h3>Fetch size</h3> * * <p> @@ -144,7 +170,7 @@ import adql.translator.TranslationException; * </i></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.1 (07/2015) + * @version 2.1 (11/2015) * @since 2.0 */ public class JDBCConnection implements DBConnection { @@ -170,6 +196,31 @@ public class JDBCConnection implements DBConnection { /** JDBC connection (created and initialized at the creation of this {@link JDBCConnection} instance). */ protected final Connection connection; + /** <p>The only {@link Statement} instance that should be used in this {@link JDBCConnection}. + * Having the same {@link Statement} for all the interactions with the database lets cancel any when needed (e.g. when the execution is too long).</p> + * <p>This statement is by default NULL ; it must be initialized by the function {@link #getStatement()}.</p> + * @since 2.1 */ + protected Statement stmt = null; + + /** + * <p>It <code>true</code>, this flag indicates that the function {@link #cancel(boolean)} has been called successfully.</p> + * + * <p>{@link #cancel(boolean)} sets this flag to <code>true</code>.</p> + * <p> + * All functions executing any kind of query on the database MUST set this flag to <code>false</code> before doing anything + * by calling the function {@link #resetCancel()}. + * </p> + * <p> + * This flag is particularly useful for debugging: when an exception is detected inside a function executing a query, + * this flag is used to know whether the exception should be ignored for logging (if <code>true</code>) or not. + * </p> + * <p> + * Any access (write AND read) to this flag MUST be synchronized on it using one of the following functions: + * {@link #cancel(boolean)}, {@link #resetCancel()} and {@link #isCancelled()}. + * </p> + * @since 2.1 */ + private Boolean cancelled = false; + /** The translator this connection must use to translate ADQL into SQL. It is also used to get information about the case sensitivity of all types of identifier (schema, table, column). */ protected final JDBCTranslator translator; @@ -198,6 +249,13 @@ public class JDBCConnection implements DBConnection { /** Indicate whether the DBMS has the notion of SCHEMA. Most of the DBMS has it, but not SQLite for instance. <i>note: If not supported, the DB table name will be prefixed by the DB schema name followed by the character "_". Nevertheless, if the DB schema name is NULL, the DB table name will never be prefixed.</i> */ protected boolean supportsSchema; + /** <p>Indicate whether a DBMS statement is able to cancel a query execution.</p> + * <p> Since this information is not provided by {@link DatabaseMetaData} a first attempt is always performed. + * In case a {@link SQLFeatureNotSupportedException} is caught, this flag is set to false preventing any further + * attempt of canceling a query.</p> + * @since 2.1 */ + protected boolean supportsCancel = true; + /* CASE SENSITIVITY SUPPORT */ /** Indicate whether UNquoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. <i>note: If FALSE, unquoted identifiers will still be considered as case insensitive for the researches, but will be stored in lower or upper case (in function of {@link #lowerCaseUnquoted} and {@link #upperCaseUnquoted}). If none of these two flags is TRUE, the storage case will be though considered as mixed.</i> */ @@ -397,11 +455,196 @@ public class JDBCConnection implements DBConnection { return connection; } + /** + * <p>Get the only statement associated with this {@link JDBCConnection}.</p> + * + * <p> + * If no {@link Statement} is yet existing, one is created, stored in this {@link JDBCConnection} (for further uses) + * and then returned. + * </p> + * + * @return The {@link Statement} instance associated with this {@link JDBCConnection}. <i>Never NULL</i> + * + * @throws SQLException In case a {@link Statement} can not be created. + * + * @since 2.1 + */ + protected Statement getStatement() throws SQLException{ + if (stmt == null || stmt.isClosed()) + return (stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)); + else + return stmt; + } + + /** + * Close the only statement associated with this {@link JDBCConnection}. + * + * @since 2.1 + */ + protected void closeStatement(){ + close(stmt); + stmt = null; + } + + /** + * <p>Cancel (and rollback when possible) the currently running query of this {@link JDBCConnection} instance.</p> + * + * <p><b>Important note:</b> + * This function is effective only if the JDBC driver and DBMS both support + * this operation. + * </p> + * <p> + * If a call of this function fails the flag {@link #supportsCancel} is set to false + * so that any subsequent call of this function for this instance of {@link JDBCConnection} + * does not try any other cancellation attempt. + * </p> + * + * <p><i>Note 1: + * A failure of a rollback is not considered as a not supported cancellation feature by the JDBC driver or the DBMS. + * So if the cancellation succeeds but a rollback fails, a next call of this function will still try cancelling the given statement. + * </i></p> + * + * <p><i>Note 2: + * In case of cancellation success, the flag {@link #cancelled} is set to <code>true</code>. + * Thus, the function executing a query can know that if any SQL exception is thrown, it will be due to the cancellation and + * should not be then considered as a real error (=> exception not logged but anyway propagated in order to stop any processing). + * </i></p></p> + * + * <p><i>Note 3: + * This function is synchronized on the {@link #cancelled} flag. + * Thus, it may block until another synchronized block on this same flag is finished. + * </i></p> + * + * @param rollback The statement to cancel. <i>Note: if closed or NULL, nothing will be done and no exception will be thrown.</i> + * + * @see DBConnection#cancel(boolean) + * @see #cancel(Statement, boolean) + * + * @since 2.1 + */ + @Override + public final void cancel(final boolean rollback){ + if (supportsCancel && stmt != null){ + synchronized(cancelled){ + cancelled = cancel(stmt, rollback); + // Log the success of the cancellation: + if (cancelled && logger != null) + logger.logDB(LogLevel.INFO, this, "CANCEL", "Query execution successfully stopped!", null); + } + } + } + + /** + * <p>Cancel (and rollback when asked and if possible) the given statement.</p> + * + * <p><b>Important note:</b> + * This function is effective only if the JDBC driver and DBMS both support + * this operation. + * </p> + * <p> + * If a call of this function fails the flag {@link #supportsCancel} is set to false + * so that any subsequent call of this function for this instance of {@link JDBCConnection} + * does not try any other cancellation attempt. + * </p> + * + * <p><i>Note: + * A failure of a rollback is not considered as a not supported cancellation feature by the JDBC driver or the DBMS. + * So if the cancellation succeeds but a rollback fails, a next call of this function will still try canceling the given statement. + * </i></p> + * + * @param stmt The statement to cancel. <i>Note: if closed or NULL, nothing will be done and no exception will be thrown.</i> + * @param rollback <code>true</code> to cancel the statement AND rollback the current connection transaction, + * <code>false</code> to just cancel the statement. + * + * @return <code>true</code> if the cancellation succeeded (or none was running), + * <code>false</code> otherwise (and especially if the "cancel" operation is not supported). + * + * @since 2.1 + */ + protected boolean cancel(final Statement stmt, final boolean rollback){ + // Not supported "cancel" operation => fail! + if (!supportsCancel) + return false; + + // No statement => "cancellation" successful! + if (stmt == null) + return true; + + // If the statement is not already closed, cancel its current query execution: + try{ + if (!stmt.isClosed()){ + // Cancel the query execution: + stmt.cancel(); + // Rollback all executed operations (only if in a transaction ; that's to say if AutoCommit = false): + if (rollback && supportsTransaction){ + try{ + if (!connection.getAutoCommit()) + connection.rollback(); + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "CANCEL", "Query execution successfully stopped BUT the rollback fails!", se); + } + } + } + return true; + }catch(SQLFeatureNotSupportedException sfnse){ + // prevent further cancel attempts: + supportsCancel = false; + // log a warning: + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "CANCEL", "This JDBC driver does not support Statement.cancel(). No further cancel attempt will be performed with this JDBCConnection instance.", sfnse); + return false; + + }catch(SQLException se){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "CANCEL", "Abortion of the current query apparently fails! The query may still run on the database server.", se); + return false; + } + } + + /** + * <p>Tell whether the last query execution has been canceled.</p> + * + * <p><i>Note: + * This function is synchronized on the {@link #cancelled} flag. + * Thus, it may block until another synchronized block on this same flag is finished. + * </i></p> + * + * @return <code>true</code> if the last query execution has been cancelled, + * <code>false</code> otherwise. + * + * @since 2.1 + */ + protected final boolean isCancelled(){ + synchronized(cancelled){ + return cancelled; + } + } + + /** + * <p>Reset the {@link #cancelled} flag to <code>false</code>.</p> + * + * <p><i>Note: + * This function is synchronized on the {@link #cancelled} flag. + * Thus, it may block until another synchronized block on this same flag is finished. + * </i></p> + * + * @since 2.1 + */ + protected final void resetCancel(){ + synchronized(cancelled){ + cancelled = false; + } + } + /* ********************* */ /* INTERROGATION METHODS */ /* ********************* */ @Override - public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException{ + public synchronized TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException{ + // Starting of new query execution => disable the cancel flag: + resetCancel(); + String sql = null; ResultSet result = null; try{ @@ -415,19 +658,25 @@ public class JDBCConnection implements DBConnection { try{ connection.setAutoCommit(false); }catch(SQLException se){ - supportsFetchSize = false; - if (logger != null) - logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + if (!isCancelled()){ + supportsFetchSize = false; + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + } } } - Statement stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + + getStatement(); + if (supportsFetchSize){ try{ stmt.setFetchSize(fetchSize); }catch(SQLException se){ - supportsFetchSize = false; - if (logger != null) - logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + if (!isCancelled()){ + supportsFetchSize = false; + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "RESULT", "Fetch size unsupported!", null); + } } } @@ -443,16 +692,19 @@ public class JDBCConnection implements DBConnection { }catch(SQLException se){ close(result); - if (logger != null) + closeStatement(); + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "EXECUTE", "Unexpected error while EXECUTING SQL query!", null); throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); }catch(TranslationException te){ close(result); + closeStatement(); if (logger != null) logger.logDB(LogLevel.ERROR, this, "TRANSLATE", "Unexpected error while TRANSLATING ADQL into SQL!", null); throw new DBException("Unexpected error while translating ADQL into SQL: " + te.getMessage(), te); }catch(DataReadException dre){ close(result); + closeStatement(); if (logger != null) logger.logDB(LogLevel.ERROR, this, "RESULT", "Unexpected error while reading the query result!", null); throw new DBException("Impossible to read the query result, because: " + dre.getMessage(), dre); @@ -460,7 +712,19 @@ public class JDBCConnection implements DBConnection { } /** - * Create a {@link TableIterator} instance which lets reading the given result table. + * <p>Create a {@link TableIterator} instance which lets reading the given result table.</p> + * + * <p><b>Important note 1:</b> + * This function also set to NULL the statement of this {@link JDBCConnection} instance: {@link #stmt}. + * However, the statement is not closed ; it is just given to a {@link ResultSetTableIterator} iterator + * which will close it in the same time as the given {@link ResultSet}, when its function + * {@link ResultSetTableIterator#close()} is called. + * </p> + * + * <p><b>Important note 2:</b> + * In case an exception occurs within this function, the {@link ResultSet} and the {@link Statement} + * are <b>immediately closed</b> before propagating the exception. + * </p> * * @param rs Result of an SQL query. * @param resultingColumns Metadata corresponding to each columns of the result. @@ -471,7 +735,19 @@ public class JDBCConnection implements DBConnection { * or if any other error occurs. */ protected TableIterator createTableIterator(final ResultSet rs, final DBColumn[] resultingColumns) throws DataReadException{ - return new ResultSetTableIterator(rs, translator, dbms, resultingColumns); + // Dis-associate the current Statement from this JDBCConnection instance: + Statement itStmt = stmt; + stmt = null; + // Return a TableIterator wrapping the given ResultSet: + try{ + return new ResultSetTableIterator(itStmt, rs, translator, dbms, resultingColumns); + }catch(Throwable t){ + // In case of any kind of exception, the ResultSet and the Statement MUST be closed in order to save resources: + close(rs); + close(itStmt); + // Then, the caught exception can be thrown: + throw (t instanceof DataReadException) ? (DataReadException)t : new DataReadException(t); + } } /* *********************** */ @@ -528,7 +804,10 @@ public class JDBCConnection implements DBConnection { * @see tap.db.DBConnection#getTAPSchema() */ @Override - public TAPMetadata getTAPSchema() throws DBException{ + public synchronized TAPMetadata getTAPSchema() throws DBException{ + // Starting of new query execution => disable the cancel flag: + resetCancel(); + // Build a virgin TAP metadata: TAPMetadata metadata = new TAPMetadata(); @@ -536,10 +815,9 @@ public class JDBCConnection implements DBConnection { TAPSchema tap_schema = TAPMetadata.getStdSchema(supportsSchema); // LOAD ALL METADATA FROM THE STANDARD TAP TABLES: - Statement stmt = null; try{ // create a common statement for all loading functions: - stmt = connection.createStatement(); + getStatement(); // load all schemas from TAP_SCHEMA.schemas: if (logger != null) @@ -562,11 +840,11 @@ public class JDBCConnection implements DBConnection { loadKeys(tap_schema.getTable(STDTable.KEYS.label), tap_schema.getTable(STDTable.KEY_COLUMNS.label), lstTables, stmt); }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create a Statement!", se); throw new DBException("Can not create a Statement!", se); }finally{ - close(stmt); + closeStatement(); } return metadata; @@ -617,7 +895,7 @@ public class JDBCConnection implements DBConnection { metadata.addSchema(newSchema); } }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load schemas from TAP_SCHEMA.schemas!", se); throw new DBException("Impossible to load schemas from TAP_SCHEMA.schemas!", se); }finally{ @@ -711,7 +989,7 @@ public class JDBCConnection implements DBConnection { return lstTables; }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load tables from TAP_SCHEMA.tables!", se); throw new DBException("Impossible to load tables from TAP_SCHEMA.tables!", se); }finally{ @@ -799,7 +1077,7 @@ public class JDBCConnection implements DBConnection { table.addColumn(newColumn); } }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); }finally{ @@ -873,7 +1151,7 @@ public class JDBCConnection implements DBConnection { while(rsKeyCols.next()) columns.put(rsKeyCols.getString(1), rsKeyCols.getString(2)); }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); throw new DBException("Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); }finally{ @@ -884,13 +1162,13 @@ public class JDBCConnection implements DBConnection { try{ sourceTable.addForeignKey(key_id, targetTable, columns, nullifyIfNeeded(description), nullifyIfNeeded(utype)); }catch(Exception ex){ - if (logger != null) + if ((ex instanceof SQLException && !isCancelled()) && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); throw new DBException("Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); } } }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); }finally{ @@ -924,8 +1202,9 @@ public class JDBCConnection implements DBConnection { * @see tap.db.DBConnection#setTAPSchema(tap.metadata.TAPMetadata) */ @Override - public void setTAPSchema(final TAPMetadata metadata) throws DBException{ - Statement stmt = null; + public synchronized void setTAPSchema(final TAPMetadata metadata) throws DBException{ + // Starting of new query execution => disable the cancel flag: + resetCancel(); try{ // A. GET THE DEFINITION OF ALL STANDARD TAP TABLES: @@ -934,7 +1213,7 @@ public class JDBCConnection implements DBConnection { startTransaction(); // B. RE-CREATE THE STANDARD TAP_SCHEMA TABLES: - stmt = connection.createStatement(); + getStatement(); // 1. Ensure TAP_SCHEMA exists and drop all its standard TAP tables: if (logger != null) @@ -960,12 +1239,12 @@ public class JDBCConnection implements DBConnection { commit(); }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "CREATE_TAP_SCHEMA", "Impossible to SET TAP_SCHEMA in DB!", se); rollback(); throw new DBException("Impossible to SET TAP_SCHEMA in DB!", se); }finally{ - close(stmt); + closeStatement(); endTransaction(); } } @@ -1639,21 +1918,23 @@ public class JDBCConnection implements DBConnection { * @see #checkUploadedTableDef(TAPTable) */ @Override - public boolean addUploadedTable(TAPTable tableDef, TableIterator data) throws DBException, DataReadException{ + public synchronized boolean addUploadedTable(TAPTable tableDef, TableIterator data) throws DBException, DataReadException{ // If no table to upload, consider it has been dropped and return TRUE: if (tableDef == null) return true; + // Starting of new query execution => disable the cancel flag: + resetCancel(); + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): checkUploadedTableDef(tableDef); - Statement stmt = null; try{ // Start a transaction: startTransaction(); // ...create a statement: - stmt = connection.createStatement(); + getStatement(); DatabaseMetaData dbMeta = connection.getMetaData(); @@ -1704,7 +1985,7 @@ public class JDBCConnection implements DBConnection { }catch(SQLException se){ rollback(); - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); throw new DBException("Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); }catch(DBException de){ @@ -1714,7 +1995,7 @@ public class JDBCConnection implements DBConnection { rollback(); throw dre; }finally{ - close(stmt); + closeStatement(); endTransaction(); } } @@ -1843,15 +2124,17 @@ public class JDBCConnection implements DBConnection { * @see #checkUploadedTableDef(TAPTable) */ @Override - public boolean dropUploadedTable(final TAPTable tableDef) throws DBException{ + public synchronized boolean dropUploadedTable(final TAPTable tableDef) throws DBException{ // If no table to upload, consider it has been dropped and return TRUE: if (tableDef == null) return true; + // Starting of new query execution => disable the cancel flag: + resetCancel(); + // Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD): checkUploadedTableDef(tableDef); - Statement stmt = null; try{ // Check the existence of the table to drop: @@ -1859,8 +2142,7 @@ public class JDBCConnection implements DBConnection { return true; // Execute the update: - stmt = connection.createStatement(); - int cnt = stmt.executeUpdate("DROP TABLE " + translator.getTableName(tableDef, supportsSchema) + ";"); + int cnt = getStatement().executeUpdate("DROP TABLE " + translator.getTableName(tableDef, supportsSchema) + ";"); // Log the end: if (logger != null){ @@ -1874,11 +2156,11 @@ public class JDBCConnection implements DBConnection { return (cnt >= 0); }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); throw new DBException("Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se); }finally{ - close(stmt); + closeStatement(); } } @@ -2208,17 +2490,32 @@ public class JDBCConnection implements DBConnection { * <p>If the given {@link Statement} is NULL, nothing (even exception/error) happens.</p> * * <p> + * The given statement is explicitly canceled by this function before being closed. + * Thus the corresponding DBMS process is ensured to be stopped. Of course, this + * cancellation is effective only if this operation is supported by the JDBC driver + * and the DBMS. + * </p> + * + * <p><b>Important note:</b> + * In case of cancellation, <b>NO</b> rollback is performed. + * </p> + * + * <p> * If any {@link SQLException} occurs during this operation, it is caught and just logged * (see {@link TAPLog#logDB(uws.service.log.UWSLog.LogLevel, DBConnection, String, String, Throwable)}). * No error is thrown and nothing else is done. * </p> * * @param stmt {@link Statement} to close. + * + * @see #cancel(Statement, boolean) */ protected final void close(final Statement stmt){ try{ - if (stmt != null) + if (stmt != null){ + cancel(stmt, false); stmt.close(); + } }catch(SQLException se){ if (logger != null) logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a Statement!", null); @@ -2624,7 +2921,8 @@ public class JDBCConnection implements DBConnection { try{ stmt.addBatch(); }catch(SQLException se){ - supportsBatchUpdates = false; + if (!isCancelled()) + supportsBatchUpdates = false; /* * If the error happens for the first row, it is still possible to insert all rows * with the non-batch function - executeUpdate(). @@ -2633,10 +2931,10 @@ public class JDBCConnection implements DBConnection { * and must stop the whole TAP_SCHEMA initialization. */ if (indRow == 1){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se); }else{ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH query impossible!", se); throw new DBException("BATCH query impossible!", se); } @@ -2689,9 +2987,11 @@ public class JDBCConnection implements DBConnection { try{ rows = stmt.executeBatch(); }catch(SQLException se){ - supportsBatchUpdates = false; - if (logger != null) - logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH execution impossible!", se); + if (!isCancelled()){ + supportsBatchUpdates = false; + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH execution impossible!", se); + } throw new DBException("BATCH execution impossible!", se); } @@ -2699,7 +2999,7 @@ public class JDBCConnection implements DBConnection { try{ stmt.clearBatch(); }catch(SQLException se){ - if (logger != null) + if (!isCancelled() && logger != null) logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "CLEAR BATCH impossible!", se); } diff --git a/src/tap/formatter/FITSFormat.java b/src/tap/formatter/FITSFormat.java index 20c7d92..13c5b85 100644 --- a/src/tap/formatter/FITSFormat.java +++ b/src/tap/formatter/FITSFormat.java @@ -36,7 +36,7 @@ import uk.ac.starlink.table.StoragePolicy; * Format any given query (table) result into FITS. * * @author Grégory Mantelet (ARI) - * @version 2.0 (04/2015) + * @version 2.1 (11/2015) * @since 2.0 */ public class FITSFormat implements OutputFormat { @@ -84,15 +84,21 @@ public class FITSFormat implements OutputFormat { ColumnInfo[] colInfos = VOTableFormat.toColumnInfos(result, execReport, thread); // Turns the result set into a table: - LimitedStarTable table = new LimitedStarTable(result, colInfos, execReport.parameters.getMaxRec()); + LimitedStarTable table = new LimitedStarTable(result, colInfos, execReport.parameters.getMaxRec(), thread); // Copy the table on disk (or in memory if the table is short): StarTable copyTable = StoragePolicy.PREFER_DISK.copyTable(table); + if (thread.isInterrupted()) + throw new InterruptedException(); + /* Format the table in FITS (2 passes are needed for that, hence the copy on disk), * and write it in the given output stream: */ new FitsTableWriter().writeStarTable(copyTable, output); + if (thread.isInterrupted()) + throw new InterruptedException(); + execReport.nbRows = table.getNbReadRows(); output.flush(); diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java index 07e7d39..923349e 100644 --- a/src/tap/formatter/VOTableFormat.java +++ b/src/tap/formatter/VOTableFormat.java @@ -83,7 +83,7 @@ import adql.db.DBType.DBDatatype; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.1 (07/2015) + * @version 2.1 (11/2015) */ public class VOTableFormat implements OutputFormat { @@ -330,7 +330,7 @@ public class VOTableFormat implements OutputFormat { ColumnInfo[] colInfos = toColumnInfos(queryResult, execReport, thread); /* Turns the result set into a table. */ - LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec()); + LimitedStarTable table = new LimitedStarTable(queryResult, colInfos, execReport.parameters.getMaxRec(), thread); /* Prepares the object that will do the serialization work. */ VOSerializer voser = VOSerializer.makeSerializer(votFormat, votVersion, table); @@ -347,6 +347,9 @@ public class VOTableFormat implements OutputFormat { execReport.nbRows = table.getNbReadRows(); out.flush(); + if (thread.isInterrupted()) + throw new InterruptedException(); + /* Check for overflow and write INFO if required. */ if (table.lastSequenceOverflowed()){ out.write("<INFO name=\"QUERY_STATUS\" value=\"OVERFLOW\"/>"); @@ -588,7 +591,7 @@ public class VOTableFormat implements OutputFormat { * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (10/2014) + * @version 2.1 (11/2015) * @since 2.0 */ public static class LimitedStarTable extends AbstractStarTable { @@ -602,6 +605,10 @@ public class VOTableFormat implements OutputFormat { /** Iterator over the data to read using this special {@link StarTable} */ private final TableIterator tableIt; + /** Thread covering this execution. If it is interrupted, the writing must stop as soon as possible. + * @since 2.1 */ + private final Thread threadToWatch; + /** Limit on the number of rows to read. Over this limit, an "overflow" event occurs and {@link #overflow} is set to TRUE. */ private final long maxrec; @@ -620,9 +627,11 @@ public class VOTableFormat implements OutputFormat { * @param tableIt Data on which to iterate using this special {@link StarTable}. * @param colInfos Information about all columns. * @param maxrec Limit on the number of rows to read. <i>(if negative, there will be no limit)</i> + * @param thread Parent thread. When an interruption is detected the writing must stop as soon as possible. */ - LimitedStarTable(final TableIterator tableIt, final ColumnInfo[] colInfos, final long maxrec){ + LimitedStarTable(final TableIterator tableIt, final ColumnInfo[] colInfos, final long maxrec, final Thread thread){ this.tableIt = tableIt; + this.threadToWatch = thread; nbCol = colInfos.length; columnInfos = colInfos; this.maxrec = maxrec; @@ -675,7 +684,7 @@ public class VOTableFormat implements OutputFormat { public boolean next() throws IOException{ irow++; try{ - if (maxrec < 0 || irow < maxrec){ + if (!threadToWatch.isInterrupted() && (maxrec < 0 || irow < maxrec)){ boolean hasNext = tableIt.nextRow(); if (hasNext){ for(int i = 0; i < nbCol && tableIt.hasNextCol(); i++) diff --git a/src/uws/job/UWSJob.java b/src/uws/job/UWSJob.java index b2a28c9..37c0995 100644 --- a/src/uws/job/UWSJob.java +++ b/src/uws/job/UWSJob.java @@ -1320,7 +1320,7 @@ public class UWSJob extends SerializableUWSObject { // Set the end time: setEndTime(new Date()); - }else if (thread == null || (thread != null && !thread.isAlive())) + }else if ((thread == null || (thread != null && !thread.isAlive())) && phase.getPhase() != ExecutionPhase.ABORTED) throw new UWSException(UWSException.BAD_REQUEST, UWSExceptionFactory.incorrectPhaseTransition(getJobId(), phase.getPhase(), ExecutionPhase.ABORTED)); }else getLogger().logJob(LogLevel.WARNING, this, "ABORT", "Abortion of the job \"" + getJobId() + "\" asked but not yet effective (after having waited " + waitForStop + "ms)!", null); @@ -1391,12 +1391,20 @@ public class UWSJob extends SerializableUWSObject { } /** - * Tells whether the thread is different from <i>null</i>, is not alive, is interrupted or is finished (see {@link JobThread#isFinished()}). + * <p>Tells whether the thread is different from <i>null</i>, is not alive or is finished (see {@link JobThread#isFinished()}).</p> + * + * <p><i><b>Important note:</b> + * Having the interrupted flag set to <code>true</code> is not enough to consider the job as stopped. + * So, if the job has been interrupted but is still running, it should mean that the {@link JobThread#jobWork()} does not + * check the interrupted flag of the thread often enough or not at the right moments. In such case, the job can not be + * considered as stopped/aborted - so the phase stays {@link ExecutionPhase#EXECUTING EXECUTING} - until the thread is "unblocked" + * and the interruption is detected. + * </i></p> * * @return <i>true</i> if the thread is not still running, <i>false</i> otherwise. */ protected final boolean isStopped(){ - return thread == null || !thread.isAlive() || thread.isInterrupted() || thread.isFinished(); + return thread == null || !thread.isAlive() || thread.isFinished(); } /** -- GitLab