package uws.job; /* * This file is part of UWSLibrary. * * UWSLibrary is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * UWSLibrary is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2015 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.OutputStream; import java.util.Date; import uws.UWSException; import uws.UWSToolBox; import uws.service.error.ServiceErrorWriter; import uws.service.file.UWSFileManager; import uws.service.log.UWSLog; import uws.service.log.UWSLog.LogLevel; /** * <P>An instance of this class is a thread dedicated to a job execution.</P> * * <P>This thread is necessary associated with a {@link UWSJob} instance. Thus the execution of this thread is mainly done in * the {@link #jobWork()} method.</P> * * <P>However its execution is possible only if the job phase is {@link ExecutionPhase#EXECUTING EXECUTING}. The job phase constraint (=EXECUTING) is * already checked so it is useless to check it again in {@link #jobWork()}.</P> * * At the end of the execution the job must be correctly filled (cf {@link #jobWork()}) according to the execution conclusion: * <ul> * <li>SUCCESS: the <i>results</i> field of job must be filled (see {@link #publishResult(Result)} and {@link #getResultOutput(Result)}),</li> * <li>ABORTED: the method {@link UWSJob#abort()} must be called or an {@link InterruptedException} can be thrown</li> * <li>ERROR: the <i>errors</i> field must be filled by calling {@link #setError(ErrorSummary)} or {@link #setError(UWSException)}.</li> * </ul> * * <P>In both cases the startTime and the endTime fields are already managed by {@link UWSJob} so it is useless to change them.</P> * * <P>Just after the job work the job phase is set to {@link ExecutionPhase#COMPLETED COMPLETED} if no interruption has been detected, * {@link ExecutionPhase#ABORTED ABORTED} otherwise.</P> * * If {@link #jobWork()} throws: * <ul> * <li>a {@link UWSException}: the method {@link #setError(UWSException)} is called. * Besides the exception is kept in the {@link JobThread#lastError} and is available thanks to the {@link UWSJob#getWorkError()}.</li> * <li>an {@link InterruptedException}: the method {@link UWSJob#abort()} is called.</li> * </ul> * * @author Grégory Mantelet (CDS;ARI) * @version 4.1 (04/2015) * * @see UWSJob#start() * @see UWSJob#abort() * @see UWSJob#getFileManager() * @see UWSJob#getWorkError() */ public abstract class JobThread extends Thread { /** The job which contains all parameters for its execution and which must be filled at the end of the execution. */ protected final UWSJob job; /** The last error which has occurred during the execution of this thread. */ protected UWSException lastError = null; /** Indicate whether the exception stored in the attribute {@link #lastError} should be considered as a grave error or not. * By default, {@link #lastError} is a "normal" error. * @since 4.1 */ protected boolean fatalError = false; /** Indicates whether the {@link #jobWork()} has been called and finished, or not. */ protected boolean finished = false; /** Description of what is done by this thread. */ protected final String taskDescription; /** * Object to use in order to write the content of an error/exception in any output stream. * If NULL, the content will be written by {@link UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream)} * (in text/plain with stack-trace). * Otherwise the content and the MIME type are determined by the error writer. * @since 4.1 */ protected final ServiceErrorWriter errorWriter; /** Group of threads in which this job thread will run. */ public final static ThreadGroup tg = new ThreadGroup("UWS_GROUP"); /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. * * @throws NullPointerException If the given job or the given file manager is null. * * @see #getDefaultTaskDescription(UWSJob) */ public JobThread(final UWSJob j) throws NullPointerException{ this(j, getDefaultTaskDescription(j), null); } /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. * @param errorWriter Object to use in case of error in order to format the details of the error for the .../error/details parameter. * * @throws NullPointerException If the given job is null. * * @see #getDefaultTaskDescription(UWSJob) * * @since 4.1 */ public JobThread(final UWSJob j, final ServiceErrorWriter errorWriter) throws NullPointerException{ this(j, getDefaultTaskDescription(j), errorWriter); } /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. * @param task Description of the task executed by this thread. * * @throws NullPointerException If the given job is null. */ public JobThread(final UWSJob j, final String task) throws NullPointerException{ super(tg, j.getJobId()); job = j; taskDescription = task; errorWriter = null; } /** * Builds the JobThread instance which will be used by the given job to execute its task. * * @param j The associated job. * @param task Description of the task executed by this thread. * @param errorWriter Object to use in case of error in order to format the details of the error for the .../error/details parameter. * * @throws NullPointerException If the given job is null. * * @since 4.1 */ public JobThread(final UWSJob j, final String task, final ServiceErrorWriter errorWriter) throws NullPointerException{ super(tg, j.getJobId()); job = j; taskDescription = task; this.errorWriter = errorWriter; } /** * Gets a default description of the task executed by a {@link JobThread}. * @param job A UWS job. * @return A default description of a JobThread which will execute the given job. */ protected final static String getDefaultTaskDescription(final UWSJob job){ // Describe the task of this thread: String task = ""; task = "Executing the job " + job.getJobId(); if (job.getJobList() != null && job.getJobList().getName() != null && !job.getJobList().getName().trim().isEmpty()){ JobList jl = job.getJobList(); task += " (JobList: " + job.getJobList().getName(); if (jl.getUWS() != null && jl.getUWS().getName() != null && !jl.getUWS().getName().trim().isEmpty()) task += ", UWS: " + jl.getUWS().getName(); task += ")"; } return task; } /** * Gets the job instance associated to this thread. * * @return The associated job instance. */ public final UWSJob getJob(){ return job; } /** * Gets the manager of the UWS files (particularly: the error and results file). * * @return The used file manager. */ public final UWSFileManager getFileManager(){ return job.getFileManager(); } /** * Gets the last error which has occurred during the execution of this thread. * * @return The last error. */ public final UWSException getError(){ return lastError; } /** * Indicates whether the {@link #jobWork()} method has been called or not. * * @return <i>true</i> if the job work is done, <i>false</i> otherwise. */ public final boolean isFinished(){ return finished; } /** * Lets changing the execution phase of the job and setting its endTime. * * @throws UWSException If there is an error while changing the execution phase. */ private final void complete() throws UWSException{ if (isInterrupted()) job.abort(); else{ job.setPhase(ExecutionPhase.COMPLETED); job.setEndTime(new Date()); } } /** * <p>Published the given error in the job.</p> * * <p><i><u>note:</u> This thread will be stopped.</i></p> * * @param error The error to publish. * * @throws UWSException If there is an error while publishing the error. * * @see UWSJob#error(ErrorSummary) */ public void setError(final ErrorSummary error) throws UWSException{ job.error(error); } /** * <p>Publishes the given exception as an error summary. * Doing that also stops the job, sets the phase to {@link ExecutionPhase#ERROR} and sets the end time.</p> * * <p> * By default, this function tries to write the stack trace of the given exception thanks to * {@link UWSFileManager#getErrorOutput(ErrorSummary, UWSJob)}. If it fails or if this job is not connected to * a job list or the connected job list is not connected to a UWS, {@link #setError(ErrorSummary)} is called and * no error file is written. * </p> * * @param ue The exception that has interrupted this job. * * @throws UWSException If there is an error while publishing the given exception. * * @see #setError(ErrorSummary) * @see UWSToolBox#writeErrorFile(Exception, ErrorSummary, UWSJob, OutputStream) */ public void setError(final UWSException ue) throws UWSException{ if (ue == null) return; try{ // Set the error summary: ErrorSummary error = new ErrorSummary(ue, ue.getUWSErrorType(), job.getUrl() + "/" + UWSJob.PARAM_ERROR_SUMMARY + "/details"); // Prepare the output stream: OutputStream output = getFileManager().getErrorOutput(error, job); // Format and write the error... // ...using the error writer, if any: if (errorWriter != null) errorWriter.writeError(ue, error, job, output); // ...or write a default output: else UWSToolBox.writeErrorFile(ue, error, job, output); // Set the error summary inside the job: setError(error); }catch(IOException ioe){ job.getLogger().logThread(LogLevel.ERROR, this, "SET_ERROR", "The stack trace of a UWSException had not been written!", ioe); setError(new ErrorSummary(ue.getMessage(), ue.getUWSErrorType())); } } /** * Creates a default result description. * * @return The created result. * * @see #createResult(String) */ public Result createResult(){ String resultName = Result.DEFAULT_RESULT_NAME; if (job.getResult(resultName) != null){ int resultCount = 0; do{ resultCount++; resultName = Result.DEFAULT_RESULT_NAME + "_" + resultCount; }while(job.getResult(resultName) != null); } return createResult(resultName); } /** * Creates a default result description but by precising its name/ID. * * @param name The name/ID of the result to create. * * @return The created result. * * @see Result#Result(UWSJob, String) */ public Result createResult(final String name){ return new Result(job, name); } /** * Publishes the given result in the job. * * @param result The result to publish. * * @throws UWSException If there is an error while publishing the result. * * @see UWSJob#addResult(Result) */ public void publishResult(final Result result) throws UWSException{ job.addResult(result); } /** * <p>Gets an output stream for the given result.</p> * * <p><i><u>note:</u> the result file will be created if needed.</i></p> * * @param resultToWrite The description of the result to write. * * @return An output stream for the given result. * * @throws IOException If there is an error while creating the file or the output stream. * @throws UWSException If an error occurs in the {@link UWSFileManager#getResultOutput(Result, UWSJob)}. */ public OutputStream getResultOutput(final Result resultToWrite) throws IOException, UWSException{ return getFileManager().getResultOutput(resultToWrite, job); } /** * <b>Gets the size of the corresponding result file.</p> * * @param result Description of the Result whose the size is wanted. * * @return The size of the corresponding result file. * * @throws IOException If there is an error while getting the result file size. * * @see UWSFileManager#getResultSize(Result, UWSJob) */ public long getResultSize(final Result result) throws IOException{ return getFileManager().getResultSize(result, job); } /** * <p>Does the job work <i>(i.e. making a long computation or executing a query on a Database)</i>.</p> * * <p><b><u>Important:</u> * <ul> * <li>This method does the job work but it MUST also fill the associated job with the execution results and/or errors.</li> * <li>Do not forget to check the interrupted flag of the thread ({@link Thread#isInterrupted()}) and then to send an {@link InterruptedException}. * Otherwise the {@link UWSJob#stop()} method will have no effect, as for {@link UWSJob#abort()} and {@link #setError(ErrorSummary)}.</li> * </ul></b></p> * * <p><i><u>Notes</u>: * <ul> * <li>The "setPhase(COMPLETED)" and the "endTime=new Date()" are automatically applied just after the call to jobWork.</li> * <li>If a {@link UWSException} is thrown the {@link JobThread} will automatically publish the exception in this job * thanks to the {@link UWSJob#error(ErrorSummary)} method or the {@link UWSJob#setErrorSummary(ErrorSummary)} method, * and so it will set its phase to {@link ExecutionPhase#ERROR}.</li> * <li>If an {@link InterruptedException} is thrown the {@link JobThread} will automatically set the phase to {@link ExecutionPhase#ABORTED}</li> * </ul></i></p> * * @throws UWSException If there is any kind of error which must be propagated. * @throws InterruptedException If the thread has been interrupted or if any method throws this exception. */ protected abstract void jobWork() throws UWSException, InterruptedException; /** * <ol> * <li>Tests the execution phase of the job: if not {@link ExecutionPhase#EXECUTING EXECUTING}, nothing is done...the thread ends immediately.</li> * <li>Calls the {@link #jobWork()} method.</li> * <li>Sets the <i>finished</i> flag to <i>true</i>.</li> * <li>Changes the job phase to {@link ExecutionPhase#COMPLETED COMPLETED} if not interrupted, else {@link ExecutionPhase#ABORTED ABORTED}. * </ol> * <P>If any {@link InterruptedException} occurs the job phase is only set to {@link ExecutionPhase#ABORTED ABORTED}.</P> * <P>If any {@link UWSException} occurs while the phase is {@link ExecutionPhase#EXECUTING EXECUTING} the job phase * is set to {@link ExecutionPhase#ERROR ERROR} and an error summary is created.</P> * <P>Whatever is the exception, it will always be available thanks to the {@link #getError()} after execution.</P> * * @see #jobWork() * @see UWSJob#setPhase(ExecutionPhase) * @see #setError(UWSException) * @see #setError(ErrorSummary) */ @Override public final void run(){ if (!job.getPhaseManager().isExecuting()) return; else{ lastError = null; finished = false; } UWSLog logger = job.getLogger(); // Log the start of this thread: logger.logThread(LogLevel.INFO, this, "START", "Thread \"" + getName() + "\" started.", null); try{ // Execute the task: jobWork(); // Change the phase to COMPLETED: finished = true; complete(); logger.logThread(LogLevel.INFO, this, "END", "Thread \"" + getName() + "\" successfully ended.", null); }catch(InterruptedException ex){ /* CASE: ABORTION * In case of abortion, the thread just stops normally, just logging an INFO saying that the thread has been cancelled. * Since it is not an abnormal behavior there is no reason to keep a trace of the interrupted exception. */ finished = true; // Abort: if (!job.stopping){ try{ job.abort(); }catch(UWSException ue){ /* Should not happen since the reason of a such exception would be that the thread can not be stopped... * ...but we are already in the thread and it is stopping. */ logger.logJob(LogLevel.WARNING, job, "ABORT", "Can not put the job in its ABORTED phase!", ue); } } // Log the abortion: logger.logThread(LogLevel.INFO, this, "END", "Thread \"" + getName() + "\" cancelled.", null); }catch(UWSException ue){ /* CASE: ERROR for a known reason * A such error is just a "normal" error, in the sense its cause is known and in a way supported or expected in * a special configuration or parameters. Thus, the error is kept and will logged with a stack trace afterwards.*/ lastError = ue; }catch(Throwable t){ /* DEFAULT: FATAL error * Any other error is considered as FATAL because it was not expected or supported at a given point. * It is generally a bug or a forgiven thing in the code of the library. As for "normal" errors, this error * is kept and will logged with stack trace afterwards. */ fatalError = true; if (t instanceof Error) lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, "A FATAL DEEP ERROR OCCURED WHILE EXECUTING THIS QUERY! This error is reported in the service logs.", ErrorType.FATAL); else if (t.getMessage() == null || t.getMessage().trim().isEmpty()) lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, t.getClass().getName(), ErrorType.FATAL); else lastError = new UWSException(UWSException.INTERNAL_SERVER_ERROR, t, ErrorType.FATAL); }finally{ finished = true; /* PUBLISH THE ERROR if any has occurred */ if (lastError != null){ // Log the error: LogLevel logLevel = fatalError ? LogLevel.FATAL : LogLevel.ERROR; logger.logJob(logLevel, job, "END", "The following " + (fatalError ? "GRAVE" : "") + " error interrupted the execution of the job " + job.getJobId() + ".", lastError); logger.logThread(logLevel, this, "END", "Thread \"" + getName() + "\" ended with an error.", null); // Set the error into the job: try{ setError(lastError); }catch(UWSException ue){ try{ logger.logThread(logLevel, this, "SET_ERROR", "[1st Attempt] Problem in JobThread.setError(UWSException), while setting the execution error of the job " + job.getJobId() + ". A last attempt will be done.", ue); setError(new ErrorSummary((lastError.getCause() != null) ? lastError.getCause().getMessage() : lastError.getMessage(), lastError.getUWSErrorType())); }catch(UWSException ue2){ logger.logThread(logLevel, this, "SET_ERROR", "[2nd and last Attempt] Problem in JobThread.setError(ErrorSummary), while setting the execution error of the job " + job.getJobId() + ". This error can not be reported to the user, but it will be reported in the log in the JOB context.", ue2); // Note: no need of a level 3: if the second attempt fails, it means the job is in a wrong phase and no error summary can never be set ; further attempt won't change anything! } } } } } }