From: Mateusz Warowny Date: Fri, 14 Jul 2023 14:01:32 +0000 (+0200) Subject: Merge branch 'feature/JAL-3954-ebi-phmmer' into mmw/JAL-4199-task-execution-update X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=refs%2Fheads%2Fmmw%2FJAL-4199-task-execution-update;p=jalview.git Merge branch 'feature/JAL-3954-ebi-phmmer' into mmw/JAL-4199-task-execution-update --- fbfa316634e1b709b5abc18345bfc65ab4982bef diff --cc src/jalview/ws2/actions/BaseTask.java index 803df3c,0000000..387ca69 mode 100644,000000..100644 --- a/src/jalview/ws2/actions/BaseTask.java +++ b/src/jalview/ws2/actions/BaseTask.java @@@ -1,310 -1,0 +1,311 @@@ +package jalview.ws2.actions; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import jalview.bin.Console; +import jalview.util.ArrayUtils; +import jalview.util.MathUtils; +import jalview.ws.params.ArgumentI; +import jalview.ws2.actions.api.TaskEventListener; +import jalview.ws2.actions.api.TaskI; +import jalview.ws2.api.Credentials; +import jalview.ws2.api.JobStatus; +import jalview.ws2.client.api.WebServiceClientI; +import jalview.ws2.helpers.DelegateJobEventListener; +import jalview.ws2.helpers.TaskEventSupport; + +import static java.lang.String.format; + +public abstract class BaseTask implements TaskI +{ + protected final long uid = MathUtils.getUID(); + + protected final WebServiceClientI webClient; + + protected final List args; + + protected final Credentials credentials; + + private final TaskEventSupport eventHandler; + + protected JobStatus status = JobStatus.CREATED; + + protected List jobs = Collections.emptyList(); + + protected R result = null; + + protected Runnable cancelAction = () -> { + }; + + protected BaseTask(WebServiceClientI webClient, List args, + Credentials credentials) + { + this.webClient = webClient; + this.args = args; + this.credentials = credentials; + this.eventHandler = new TaskEventSupport<>(this); + } + + @Override + public final long getUid() + { + return uid; + } + + @Override + public final JobStatus getStatus() + { + return status; + } + + @Override + public final List getSubJobs() + { + return jobs; + } + + @Override + public final void addTaskEventListener(TaskEventListener listener) + { + eventHandler.addListener(listener); + } + + @Override + public final void removeTaskEventListener(TaskEventListener listener) + { + eventHandler.addListener(listener); + } + + @Override + public final R getResult() + { + return result; + } + + @Override + public final void init() throws Exception + { + try + { + jobs = prepareJobs(); + } catch (ServiceInputInvalidException e) + { + setStatus(JobStatus.INVALID); + eventHandler.fireTaskException(e); + throw e; + } + setStatus(JobStatus.READY); + eventHandler.fireTaskStarted(jobs); + var jobListener = new DelegateJobEventListener<>(eventHandler); + for (var job : jobs) + job.addPropertyChangeListener(jobListener); + submitJobs(jobs); + } + + static final int MAX_SUBMIT_RETRY = 5; + + protected final void submitJobs(List jobs) throws IOException + { + var retryCounter = 0; + while (true) + { + try + { + submitJobs0(jobs); + setStatus(JobStatus.SUBMITTED); + break; + } catch (IOException e) + { + eventHandler.fireTaskException(e); + if (++retryCounter > MAX_SUBMIT_RETRY) + { + cancel(); + setStatus(JobStatus.SERVER_ERROR); + throw e; + } + } + } + } + + private final void submitJobs0(List jobs) throws IOException + { + IOException exception = null; + for (BaseJob job : jobs) + { + if (job.getStatus() != JobStatus.READY || !job.isInputValid()) + continue; + try + { + var jobRef = webClient.submit(job.getInputSequences(), args, credentials); + job.setServerJob(jobRef); + job.setStatus(JobStatus.SUBMITTED); + } catch (IOException e) + { + exception = e; + } + } + if (exception != null) + throw exception; + } + + /** + * Poll all running jobs and update their status and logs. Polling is repeated + * periodically until this method return true when all jobs are done. + * + * @return {@code true] if all jobs are done @throws IOException if server + * error occurred + */ + @Override + public final boolean poll() throws IOException + { + boolean allDone = true; + IOException exception = null; + for (BaseJob job : jobs) + { + if (job.isInputValid() && !job.getStatus().isDone()) + { + var serverJob = job.getServerJob(); + try + { + job.setStatus(webClient.getStatus(serverJob)); + job.setLog(webClient.getLog(serverJob)); + job.setErrorLog(webClient.getErrorLog(serverJob)); + } catch (IOException e) + { + exception = e; + } + } + allDone &= job.isCompleted(); + } + updateGlobalStatus(); + if (exception != null) + throw exception; + return allDone; + } + + @Override + public final void complete() throws IOException + { + for (var job : jobs) + { + if (!job.isCompleted()) + { + // a fallback in case the executor decides to finish prematurely + cancelJob(job); + job.setStatus(JobStatus.SERVER_ERROR); + } + } + updateGlobalStatus(); + try { + result = collectResult(jobs); + eventHandler.fireTaskCompleted(result); + } + catch (Exception e) + { + eventHandler.fireTaskException(e); + throw e; + } + } + + /** + * Cancel all running jobs. Used in case of task failure to cleanup the + * resources or when the task has been cancelled. + */ + @Override + public final void cancel() + { + cancelAction.run(); + for (T job : jobs) + { + cancelJob(job); + } ++ setStatus(JobStatus.CANCELLED); + } + + private final void cancelJob(T job) + { + if (!job.isCompleted()) + { + try + { + if (job.getServerJob() != null) + webClient.cancel(job.getServerJob()); + job.setStatus(JobStatus.CANCELLED); + } catch (IOException e) + { + Console.error(format("failed to cancel job %s", job.getServerJob()), e); + } + } + } + + protected final void setStatus(JobStatus status) + { + Objects.requireNonNull(status); + if (this.status != status) + { + this.status = status; + eventHandler.fireTaskStatusChanged(status); + } + } + + protected abstract List prepareJobs() throws ServiceInputInvalidException; + + protected abstract R collectResult(List jobs) throws IOException; + + /** + * Update task status according to the overall status of its jobs. The rules + * of setting the status are following: + *
    + *
  • task is invalid if all jobs are invalid
  • + *
  • task is completed if all but invalid jobs are completed
  • + *
  • task is ready, submitted or queued if at least one job is ready, + * submitted or queued an none proceeded to the next stage excluding + * completed.
  • + *
  • task is running if at least one job is running and none are failed or + * cancelled
  • + *
  • task is cancelled if at least one job is cancelled and none failed
  • + *
  • task is failed or server error if at least one job is failed or server + * error
  • + *
+ */ + protected final void updateGlobalStatus() + { + int precedence = -1; + for (BaseJob job : jobs) + { + JobStatus status = job.getStatus(); + int jobPrecedence = ArrayUtils.indexOf(JobStatus.statusPrecedence, status); + if (precedence < jobPrecedence) + precedence = jobPrecedence; + } + if (precedence >= 0) + { + setStatus(JobStatus.statusPrecedence[precedence]); + } + } + + /** + * Set the action that will be run when the {@link #cancel()} method is + * invoked. The action should typically stop the executor polling the task and + * release resources and threads running the task. + * + * @param action + * runnable to be executed when the task is cancelled + */ + public void setCancelAction(Runnable action) + { + Objects.requireNonNull(action); + this.cancelAction = action; + } + + @Override + public String toString() + { + var statusName = status != null ? status.name() : "UNSET"; + return String.format("%s(%x, %s)", getClass().getSimpleName(), uid, statusName); + } +} diff --cc src/jalview/ws2/actions/NullAction.java index 0000000,655a133..f91cef6 mode 000000,100644..100644 --- a/src/jalview/ws2/actions/NullAction.java +++ b/src/jalview/ws2/actions/NullAction.java @@@ -1,0 -1,52 +1,51 @@@ + package jalview.ws2.actions; + + import java.util.List; + ++import jalview.api.AlignViewportI; + import jalview.viewmodel.AlignmentViewport; + import jalview.ws.params.ArgumentI; + import jalview.ws2.actions.api.ActionI; -import jalview.ws2.actions.api.TaskEventListener; + import jalview.ws2.actions.api.TaskI; + import jalview.ws2.api.Credentials; + + /** + * An empty implementation of the {@link ActionI} interface that does nothing. + * Use as a placeholder for testing purposes. + * + * @author mmwarowny + * + */ + public final class NullAction extends BaseAction + { + public static final class Builder extends BaseAction.Builder + { + public NullAction build() + { + return new NullAction(this); + } + } + + public static Builder newBuilder() + { + return new Builder(); + } + + protected NullAction(Builder builder) + { + super(builder); + } + + @Override - public TaskI perform(AlignmentViewport viewport, - List args, Credentials credentials, - TaskEventListener handler) ++ public TaskI createTask(AlignViewportI viewport, ++ List args, Credentials credentials) + { + return new NullTask(); + } + + @Override + public boolean isActive(AlignmentViewport viewport) + { + return false; + } + } diff --cc src/jalview/ws2/actions/NullTask.java index 0000000,223b9fb..5dd5ab0 mode 000000,100644..100644 --- a/src/jalview/ws2/actions/NullTask.java +++ b/src/jalview/ws2/actions/NullTask.java @@@ -1,0 -1,47 +1,74 @@@ + package jalview.ws2.actions; + + import java.util.Collections; + import java.util.List; + + import jalview.ws2.actions.api.JobI; ++import jalview.ws2.actions.api.TaskEventListener; + import jalview.ws2.actions.api.TaskI; + import jalview.ws2.api.JobStatus; + + /** - * An empty task returned by the {@link NullAction}. Use as a placeholder - * for testing purposes. ++ * An empty task returned by the {@link NullAction}. Use as a placeholder for ++ * testing purposes. + * + * @author mmwarowny + * + */ + class NullTask implements TaskI + { + @Override + public long getUid() + { + return 0; + } + + @Override + public JobStatus getStatus() + { + return JobStatus.READY; + } + + @Override + public List getSubJobs() + { + return Collections.emptyList(); + } + + @Override ++ public void init() throws Exception ++ { ++ } ++ ++ @Override ++ public boolean poll() throws Exception ++ { ++ return true; ++ } ++ ++ @Override ++ public void complete() throws Exception ++ { ++ } ++ ++ @Override + public Void getResult() + { + return null; + } + + @Override + public void cancel() + { + } ++ ++ @Override ++ public void addTaskEventListener(TaskEventListener listener) ++ { ++ } ++ ++ @Override ++ public void removeTaskEventListener(TaskEventListener listener) ++ { ++ } + } diff --cc src/jalview/ws2/actions/api/TaskEventListener.java index 4bc79d0,7427aa8..b0bb4b6 --- a/src/jalview/ws2/actions/api/TaskEventListener.java +++ b/src/jalview/ws2/actions/api/TaskEventListener.java @@@ -97,5 -108,62 +98,56 @@@ public interface TaskEventListener source, JobI job, String log); + default void subJobErrorLogChanged(TaskI source, JobI job, String log) {}; + + @SuppressWarnings("rawtypes") + static final TaskEventListener NULL_LISTENER = new TaskEventListener() + { + @Override + public void taskStarted(TaskI source, List subJobs) + { + Console.info("task started with " + subJobs.size() + " jobs"); + } + + @Override + public void taskStatusChanged(TaskI source, JobStatus status) + { + Console.info("task status " + status); + } + + @Override + public void taskCompleted(TaskI source, Object result) + { + Console.info("task completed"); + } + + @Override + public void taskException(TaskI source, Exception e) + { + Console.info("task failed", e); + } + + @Override - public void taskRestarted(TaskI source) - { - Console.info("task restarted"); - } - - @Override + public void subJobStatusChanged(TaskI source, JobI job, + JobStatus status) + { + Console.info("sub-job " + job.getInternalId() + " status " + status); + } + + @Override + public void subJobLogChanged(TaskI source, JobI job, String log) + { + } + + @Override + public void subJobErrorLogChanged(TaskI source, JobI job, String log) + { + } + }; + + @SuppressWarnings("unchecked") + static TaskEventListener nullListener() + { + return (TaskEventListener) NULL_LISTENER; + } } diff --cc src/jalview/ws2/actions/hmmer/PhmmerAction.java index 0000000,5e6ef08..bdba2f7 mode 000000,100644..100644 --- a/src/jalview/ws2/actions/hmmer/PhmmerAction.java +++ b/src/jalview/ws2/actions/hmmer/PhmmerAction.java @@@ -1,0 -1,73 +1,82 @@@ + package jalview.ws2.actions.hmmer; + + import java.util.List; + import java.util.Objects; + ++import jalview.api.AlignViewportI; + import jalview.datamodel.AlignmentI; + import jalview.viewmodel.AlignmentViewport; + import jalview.ws.params.ArgumentI; + import jalview.ws2.actions.BaseAction; ++import jalview.ws2.actions.PollingTaskExecutor; + import jalview.ws2.actions.api.TaskEventListener; + import jalview.ws2.actions.api.TaskI; + import jalview.ws2.api.Credentials; + import jalview.ws2.client.api.AlignmentWebServiceClientI; + import jalview.ws2.client.api.WebServiceClientI; + + /** + * Implementation of the {@link BaseAction} for the phmmer client. This is NOT + * how you should implement it. The action should be more generic and cover + * range of similar services. + * + * @author mmwarowny + * + */ + // FIXME: Not an alignment action (temporary hack) + public class PhmmerAction extends BaseAction + { + public static class Builder extends BaseAction.Builder + { + protected AlignmentWebServiceClientI client; + + private Builder(AlignmentWebServiceClientI client) + { + super(); + Objects.requireNonNull(client); + this.client = client; + } + + public PhmmerAction build() + { + return new PhmmerAction(this); + } + } + + public static Builder newBuilder(AlignmentWebServiceClientI client) + { + return new Builder(client); + } + + protected final AlignmentWebServiceClientI client; + + public PhmmerAction(Builder builder) + { + super(builder); + client = builder.client; + } + - @Override + public TaskI perform(AlignmentViewport viewport, + List args, Credentials credentials, + TaskEventListener handler) + { - var task = new PhmmerTask(client, args, credentials, - viewport.getAlignmentView(true), handler); - task.start(viewport.getServiceExecutor()); ++ var task = createTask(viewport, args, credentials); ++ task.addTaskEventListener(handler); ++ var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); ++ var future = executor.submit(task); ++ task.setCancelAction(() -> { future.cancel(true); }); + return task; + } ++ ++ public PhmmerTask createTask(AlignViewportI viewport, ++ List args, Credentials credentials) ++ { ++ return new PhmmerTask(client, args, credentials, viewport.getAlignmentView(true)); ++ } + + @Override + public boolean isActive(AlignmentViewport viewport) + { + return false; + } + } diff --cc src/jalview/ws2/actions/hmmer/PhmmerTask.java index 0000000,8a7a826..ede61d3 mode 000000,100644..100644 --- a/src/jalview/ws2/actions/hmmer/PhmmerTask.java +++ b/src/jalview/ws2/actions/hmmer/PhmmerTask.java @@@ -1,0 -1,114 +1,111 @@@ + package jalview.ws2.actions.hmmer; + + import static jalview.util.Comparison.GapChars; + + import java.io.IOException; -import java.util.Arrays; + import java.util.List; + + import jalview.analysis.AlignSeq; + import jalview.bin.Console; + import jalview.datamodel.AlignmentAnnotation; + import jalview.datamodel.AlignmentI; + import jalview.datamodel.AlignmentView; + import jalview.datamodel.Annotation; + import jalview.datamodel.Sequence; + import jalview.datamodel.SequenceI; + import jalview.util.Comparison; + import jalview.ws.params.ArgumentI; -import jalview.ws2.actions.AbstractPollableTask; + import jalview.ws2.actions.BaseJob; ++import jalview.ws2.actions.BaseTask; + import jalview.ws2.actions.ServiceInputInvalidException; -import jalview.ws2.actions.api.TaskEventListener; + import jalview.ws2.api.Credentials; + import jalview.ws2.api.JobStatus; + import jalview.ws2.client.api.AlignmentWebServiceClientI; + -class PhmmerTask extends AbstractPollableTask ++class PhmmerTask extends BaseTask + { + private final AlignmentWebServiceClientI client; + private final AlignmentView view; + + PhmmerTask(AlignmentWebServiceClientI client, List args, - Credentials credentials, AlignmentView view, - TaskEventListener eventListener) ++ Credentials credentials, AlignmentView view) + { - super(client, args, credentials, eventListener); ++ super(client, args, credentials); + this.client = client; + this.view = view; + } + + @Override - protected List prepare() throws ServiceInputInvalidException ++ protected List prepareJobs() throws ServiceInputInvalidException + { + Console.info("Preparing sequence for phmmer job"); + var sequence = view.getVisibleAlignment('-').getSequenceAt(0); + var seq = new Sequence(sequence.getName(), + AlignSeq.extractGaps(GapChars, sequence.getSequenceAsString())); + var job = new BaseJob(List.of(seq)) + { + @Override + public boolean isInputValid() + { + return true; + } + }; + job.setStatus(JobStatus.READY); + return List.of(job); + } + + @Override - protected AlignmentI done() throws IOException ++ protected AlignmentI collectResult(List jobs) throws IOException + { - var job = getSubJobs().get(0); ++ var job = jobs.get(0); + var status = job.getStatus(); + Console.info(String.format("phmmer finished job \"%s\" with status %s", + job.getServerJob().getJobId(), status)); + if (status != JobStatus.COMPLETED) + return null; + var outputAlignment = client.getAlignment(job.getServerJob()); + var querySeq = job.getInputSequences().get(0).deriveSequence(); + { + AlignmentAnnotation refpos = null; + for (var annot : outputAlignment.getAlignmentAnnotation()) + { + if (annot.sequenceRef == null && annot.label.equals("Reference Positions")) + { + refpos = annot; + break; + } + } + if (refpos != null) + { + querySeq = alignQeuryToReferencePositions(querySeq, refpos); + } + } + outputAlignment.insertSequenceAt(0, querySeq); + return outputAlignment; + } + + private SequenceI alignQeuryToReferencePositions(SequenceI query, AlignmentAnnotation refpos) + { + var sequenceBuilder = new StringBuilder(); + var index = 0; + for (Annotation a : refpos.annotations) + { + // TODO: we assume that the number of "x" annotations is equal to the number + // of residues. may need a safeguard against invalid input + if (a != null && a.displayCharacter.equals("x")) + { + char c; + do + c = query.getCharAt(index++); + while (Comparison.isGap(c)); + sequenceBuilder.append(c); + } + else + { + sequenceBuilder.append(Comparison.GAP_DASH); + } + } + query.setSequence(sequenceBuilder.toString()); + return query; + } + } diff --cc src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java index 0000000,a20575d..48d0ba5 mode 000000,100644..100644 --- a/src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java +++ b/src/jalview/ws2/client/ebi/JobDispatcherWSDiscoverer.java @@@ -1,0 -1,108 +1,107 @@@ + package jalview.ws2.client.ebi; + + import java.io.IOException; + import java.net.MalformedURLException; + import java.net.URISyntaxException; + import java.net.URL; + import java.util.List; + + import jalview.bin.Console; + import jalview.ws.params.ParamManager; + import jalview.ws2.actions.NullAction; + import jalview.ws2.actions.api.ActionI; + import jalview.ws2.actions.hmmer.PhmmerAction; + import jalview.ws2.api.WebService; + import jalview.ws2.client.api.AbstractWebServiceDiscoverer; + import uk.ac.dundee.compbio.hmmerclient.PhmmerClient; + + public final class JobDispatcherWSDiscoverer extends AbstractWebServiceDiscoverer + { + + private static final URL DEFAULT_URL; + static + { + try + { + DEFAULT_URL = new URL("https://www.ebi.ac.uk/Tools/services/rest/hmmer3_phmmer/"); + } catch (MalformedURLException e) + { + throw new ExceptionInInitializerError(e); + } + } + + private static JobDispatcherWSDiscoverer instance = null; + private static ParamManager paramManager = null; + + private JobDispatcherWSDiscoverer() {} + + public static JobDispatcherWSDiscoverer getInstance() { + if (instance == null) + instance = new JobDispatcherWSDiscoverer(); + return instance; + } + + public static void setParamManager(ParamManager manager) + { + paramManager = manager; + } + + @Override + public int getStatusForUrl(URL url) + { + try + { + return new PhmmerClient(url).testEndpoint() ? STATUS_OK : STATUS_INVALID; + } catch (URISyntaxException e) + { + Console.error(e.getMessage()); + return STATUS_INVALID; + } + } + + @Override + protected String getUrlsPropertyKey() + { + return null; + } + + @Override + protected URL getDefaultUrl() + { + return DEFAULT_URL; + } + + @Override + protected List> fetchServices(URL url) throws IOException + { + PhmmerClient phmmerClient; + try { + phmmerClient = new PhmmerClient(url); + } + catch (URISyntaxException e) { + throw new MalformedURLException(e.getMessage()); + } + if (!phmmerClient.testEndpoint()) + throw new IOException( + "unable to reach dispatcher server at " + url); - // TODO change once a concrete action is implemented + var wsBuilder = WebService. newBuilder(); + wsBuilder.url(url); + wsBuilder.clientName("job dispatcher"); + wsBuilder.category("Database search"); + wsBuilder.name("pHMMER"); + wsBuilder.description("Hmmer3 phmmer is used to search one or more query sequences against a sequence database."); + wsBuilder.interactive(false); + wsBuilder.paramDatastore(ParamStores.newPhmmerDatastore(url, paramManager)); + wsBuilder.actionClass(PhmmerAction.class); + var webService = wsBuilder.build(); + + var client = new PhmmerWSClient(phmmerClient); + var actionBuilder = PhmmerAction.newBuilder(client); + actionBuilder.webService(webService); + actionBuilder.name(""); + webService.addAction(actionBuilder.build()); + + return List.of(webService); + } + + } diff --cc src/jalview/ws2/gui/SearchServiceGuiHandler.java index 0000000,519adc7..49df466 mode 000000,100644..100644 --- a/src/jalview/ws2/gui/SearchServiceGuiHandler.java +++ b/src/jalview/ws2/gui/SearchServiceGuiHandler.java @@@ -1,0 -1,242 +1,236 @@@ + package jalview.ws2.gui; + + import java.util.List; + + import javax.swing.SwingUtilities; + + import jalview.bin.Console; + import jalview.datamodel.Alignment; + import jalview.datamodel.AlignmentAnnotation; + import jalview.datamodel.AlignmentI; + import jalview.gui.AlignFrame; + import jalview.gui.Desktop; + import jalview.gui.JvOptionPane; + import jalview.gui.WebserviceInfo; + import jalview.util.ArrayUtils; + import jalview.util.MessageManager; + import jalview.ws2.actions.api.ActionI; + import jalview.ws2.actions.api.JobI; + import jalview.ws2.actions.api.TaskEventListener; + import jalview.ws2.actions.api.TaskI; + import jalview.ws2.api.JobStatus; + import jalview.ws2.api.WebService; + import jalview.ws2.helpers.WSClientTaskWrapper; + + import static java.lang.String.format; + + class SearchServiceGuiHandler implements TaskEventListener + { + private final AlignFrame parentFrame; + + private final ActionI action; + + private final WebService service; + + private WebserviceInfo infoPanel; + + private JobI[] jobs = new JobI[0]; + + private int[] tabs = new int[0]; + + private int[] logOffset = new int[0]; + + private int[] errLogOffset = new int[0]; + + public SearchServiceGuiHandler(ActionI action, AlignFrame parentFrame) + { + this.parentFrame = parentFrame; + this.action = action; + this.service = action.getWebService(); + var info = String.format("%s search using service at %s%n%s", + service.getName(), service.getUrl(), service.getDescription()); + this.infoPanel = new WebserviceInfo(service.getName(), info, false); + } + + @Override + public void taskStarted(TaskI source, + List subJobs) + { + Console.debug(format("task %s#%x started with %d sub-jobs", + service.getName(), source.getUid(), subJobs.size())); + jobs = subJobs.toArray(new JobI[subJobs.size()]); + tabs = new int[subJobs.size()]; + logOffset = new int[subJobs.size()]; + errLogOffset = new int[subJobs.size()]; + for (int i = 0; i < subJobs.size(); i++) + { + JobI job = jobs[i]; + int tabIndex = infoPanel.addJobPane(); + tabs[i] = tabIndex; + infoPanel.setProgressName(format("region %d", i), tabIndex); + infoPanel.setProgressText(tabIndex, "Job details:\n"); + // jobs should not have states other than invalid or ready at this point + if (job.getStatus() == JobStatus.INVALID) + infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_STOPPED_OK); + else if (job.getStatus() == JobStatus.READY) + infoPanel.setStatus(tabIndex, WebserviceInfo.STATE_QUEUING); + } + } + + @Override + public void taskStatusChanged(TaskI source, JobStatus status) + { + Console.debug(format("task %s#%x status changed to %s", + service.getName(), source.getUid(), status)); + switch (status) + { + case INVALID: + infoPanel.setVisible(false); + JvOptionPane.showMessageDialog(parentFrame, + MessageManager.getString("info.invalid_search_input"), + MessageManager.getString("info.invalid_search_input"), + JvOptionPane.INFORMATION_MESSAGE); + break; + case READY: + infoPanel.setthisService(new WSClientTaskWrapper(source)); + infoPanel.setVisible(true); + // intentional no break + case SUBMITTED: + case QUEUED: + infoPanel.setStatus(WebserviceInfo.STATE_QUEUING); + break; + case RUNNING: + case UNKNOWN: // unsure what to do with unknown + infoPanel.setStatus(WebserviceInfo.STATE_RUNNING); + break; + case COMPLETED: + infoPanel.setProgressBar( + MessageManager.getString("status.collecting_job_results"), + jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_OK); + break; + case FAILED: + infoPanel.removeProgressBar(jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); + break; + case CANCELLED: + infoPanel.setStatus(WebserviceInfo.STATE_CANCELLED_OK); + break; + case SERVER_ERROR: + infoPanel.removeProgressBar(jobs[0].getInternalId()); + infoPanel.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR); + break; + } + } + + @Override + public void taskCompleted(TaskI source, AlignmentI result) + { + Console.debug(format("task %s#%x completed", service.getName(), + source.getUid())); + SwingUtilities.invokeLater( + () -> infoPanel.removeProgressBar(jobs[0].getInternalId())); + if (result == null) + { + SwingUtilities.invokeLater(infoPanel::setFinishedNoResults); + return; + } + infoPanel.showResultsNewFrame.addActionListener(evt -> { + // copy alignment for each frame to have its own instance + var alnCpy = new Alignment(result); + alnCpy.setGapCharacter(result.getGapCharacter()); + alnCpy.setDataset(result.getDataset()); + for (AlignmentAnnotation annotation : result.getAlignmentAnnotation()) + alnCpy.addAnnotation(new AlignmentAnnotation(annotation)); + displayResultsNewFrame(alnCpy); + }); + SwingUtilities.invokeLater(infoPanel::setResultsReady); + } + + private void displayResultsNewFrame(AlignmentI aln) + { + AlignFrame frame = new AlignFrame(aln, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + frame.getFeatureRenderer().transferSettings( + parentFrame.getFeatureRenderer().getSettings()); + var actionName = action.getName() != null ? action.getName() : "Search"; + var title = String.format("%s %s of %s", service.getName(), actionName, + parentFrame.getTitle()); + Desktop.addInternalFrame(frame, title, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + } + + @Override + public void taskException(TaskI source, Exception e) + { + Console.error(format("Task %s#%x raised an exception.", + service.getName(), source.getUid()), e); + infoPanel.appendProgressText(e.getMessage()); + } + + @Override - public void taskRestarted(TaskI source) - { - // search services non-restartable - } - - @Override + public void subJobStatusChanged(TaskI source, JobI job, + JobStatus status) + { + Console.debug(format("sub-job %x status changed to %s", + job.getInternalId(), status)); + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should not happen irl + return; + int wsStatus; + switch (status) + { + case INVALID: + case COMPLETED: + wsStatus = WebserviceInfo.STATE_STOPPED_OK; + break; + case READY: + case SUBMITTED: + case QUEUED: + wsStatus = WebserviceInfo.STATE_QUEUING; + break; + case RUNNING: + case UNKNOWN: + wsStatus = WebserviceInfo.STATE_RUNNING; + break; + case FAILED: + wsStatus = WebserviceInfo.STATE_STOPPED_ERROR; + break; + case CANCELLED: + wsStatus = WebserviceInfo.STATE_CANCELLED_OK; + break; + case SERVER_ERROR: + wsStatus = WebserviceInfo.STATE_STOPPED_SERVERERROR; + break; + default: + throw new AssertionError("Non-exhaustive switch statement"); + } + infoPanel.setStatus(tabs[i], wsStatus); + } + + @Override + public void subJobLogChanged(TaskI source, JobI job, + String log) + { + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should never happen + return; + infoPanel.appendProgressText(tabs[i], log.substring(logOffset[i])); + } + + @Override + public void subJobErrorLogChanged(TaskI source, JobI job, + String log) + { + int i = ArrayUtils.indexOf(jobs, job); + assert i >= 0 : "job does not exist"; + if (i < 0) + // safeguard that should never happen + return; + infoPanel.appendProgressText(tabs[i], log.substring(errLogOffset[i])); + } + } diff --cc src/jalview/ws2/gui/WebServicesMenuManager.java index 516f0ba,12aeaa9..3757199 --- a/src/jalview/ws2/gui/WebServicesMenuManager.java +++ b/src/jalview/ws2/gui/WebServicesMenuManager.java @@@ -21,6 -24,7 +21,8 @@@ import javax.swing.JMenuItem import javax.swing.ToolTipManager; import javax.swing.border.EmptyBorder; + import jalview.bin.Console; ++import jalview.datamodel.AlignmentI; import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.JvSwingUtils; @@@ -30,14 -34,12 +32,16 @@@ import jalview.viewmodel.AlignmentViewp import jalview.ws.params.ArgumentI; import jalview.ws.params.ParamDatastoreI; import jalview.ws.params.WsParamSetI; +import jalview.ws2.actions.BaseTask; +import jalview.ws2.actions.PollingTaskExecutor; import jalview.ws2.actions.alignment.AlignmentAction; +import jalview.ws2.actions.alignment.AlignmentResult; +import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter; import jalview.ws2.actions.annotation.AnnotationAction; import jalview.ws2.actions.api.ActionI; + import jalview.ws2.actions.api.TaskEventListener; import jalview.ws2.actions.api.TaskI; + import jalview.ws2.actions.hmmer.PhmmerAction; import jalview.ws2.api.Credentials; import jalview.ws2.api.WebService; import jalview.ws2.client.api.WebServiceProviderI; @@@ -444,29 -467,21 +448,44 @@@ public class WebServicesMenuManage } if (action instanceof AnnotationAction) { + var calcManager = viewport.getCalcManager(); + var _action = (AnnotationAction) action; + var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel, + _action, args, credentials); var handler = new AnnotationServiceGuiHandler(_action, frame); - return _action.perform(viewport, args, credentials, handler); + worker.setWorkerListener(handler); + for (var w : calcManager.getWorkers()) + { + if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName())) + { + calcManager.cancelWorker(w); + calcManager.removeWorker(w); + } + } + if (action.getWebService().isInteractive()) + calcManager.registerWorker(worker); + else + calcManager.startWorker(worker); + return; } - throw new IllegalArgumentException( - String.format("Illegal action type %s", action.getClass().getName())); + if (action instanceof PhmmerAction) + { + var _action = (PhmmerAction) action; + var handler = new SearchServiceGuiHandler(_action, frame); - return _action.perform(viewport, args, credentials, handler); ++ TaskI task = _action.createTask(viewport, args, credentials); ++ var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor()); ++ task.addTaskEventListener(handler); ++ _action.perform(viewport, args, credentials, handler); ++ return; + } + Console.warn(String.format( + "No known handler for action type %s. All output will be discarded.", + action.getClass().getName())); - return action.perform(viewport, args, credentials, - TaskEventListener.nullListener()); ++ var task = action.createTask(viewport, args, credentials); ++ task.addTaskEventListener(TaskEventListener.nullListener()); ++ PollingTaskExecutor.fromPool(viewport.getServiceExecutor()) ++ .submit(task); } private static CompletionStage> openEditParamsDialog(