diff --git a/src/tap/ADQLExecutor.java b/src/tap/ADQLExecutor.java index 27ec125ac116d6d36c8cb72b14bd871ba2970214..841e023090afa134b58f7ff194b6fe598c6b56bc 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 aa89491428ac40e8d058d38b58fac92a7b6102a1..c2e2ea974465d22cf5d3fbfc1d49911a7c858b9e 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 02432f78d919c12b7588d28d34f82c946a1394bd..d42e2621f20bb9c7db0ce7ae7b1e3acd1e542f94 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 af4fdeac67d4a91ae82a04a0bc50778f680b314d..f5e7684fdb4da690f4ba3e12fbeebeefd51dcfe7 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 e836211115b3159bae26c789792821f3de27b892..612954ed2a65731e9da8c28daa7e414171eb0f80 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 f8dc32d10fb854bcc3716424b79ca7e869a228a5..d8ed5977c67f96b46886cf56a38922299cbbfcf6 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 20c7d928c84661d522ec3b17643db904840ce382..13c5b85e06e3a0dda7d549ecead3ce063703b43c 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 07e7d398f8b02df899a44cad8b9490147d705bb7..923349e24e1281fa2fe0daceb7bc6c891f274605 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 b2a28c9f367b13560925b963c2bdec220571e065..37c09951e6da9aca01bfd26712ac4de3017d0841 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(); } /**