/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.work; import java.io.File; import java.nio.file.Paths; import java.util.HashSet; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.joda.time.DateTime; import divconq.hub.Hub; import divconq.lang.op.FuncResult; import divconq.lang.op.IOperationLogger; import divconq.lang.op.IOperationObserver; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationEvents; import divconq.lang.op.OperationResult; import divconq.struct.RecordStruct; import divconq.struct.Struct; import divconq.util.FileUtil; import divconq.util.StringUtil; /** * Do not run same task object in parallel * */ public class TaskRun extends FuncResult<Struct> implements Runnable { protected Task task = null; protected long started = -1; protected long lastclaimed = -1; protected int slot = 0; protected boolean completed = false; protected boolean killed = false; protected final Lock completionlock = new ReentrantLock(); // TODO consider StampedLock protected Set<AutoCloseable> closeables = new HashSet<>(); public boolean hasStarted() { return (this.started > -1); } // don't alter this after submitting to work pool, this is for view only as submit public Task getTask() { return this.task; } public TaskRun() { super(); this.task = new Task(); this.task.withSubContext(); this.msgStart = 0; } public TaskRun(Task info) { super(); this.task = info; this.msgStart = 0; } // prep is running in external context, log to that context but public void prep() { // if we are resuming, leave the rest alone if (this.started != -1) return; this.task.prep(); OperationContext ctx = this.task.getContext(); this.opcontext = ctx; this.markStart(); // add any new observers for (IOperationObserver ob : this.task.getObservers()) ctx.addObserver(ob); ctx.fireEvent(OperationEvents.PREP_TASK, null); /* TODO cleanup // loop task observers from Task as well as added at run time for (IOperationObserver cb : this.observers) { try { if (cb instanceof ITaskObserver) ((ITaskObserver)cb).prep(this); } catch (Exception x) { this.error("Error notifying completing task: " + x); } // they might change context on us, return context OperationContext.set(this.opcontext); } */ } public boolean isComplete() { return this.completed; } public boolean isKilled() { return this.killed; } // must report if timed out, even if completed - otherwise Worker thread might lock forever if WorkBucket kills us first public boolean isHung() { return this.isInactive() || this.isOverdue(); } public boolean isInactive() { long timeout = this.task.getTimeoutMS(); //System.out.println("Get last activity in active test: " + this.getLastActivity()); // has activity been quiet for longer than timeout? if ((timeout > 0) && (this.getLastActivity() < (System.currentTimeMillis() - timeout))) return true; return false; } // only become overdue after it has started public boolean isOverdue() { long deadline = this.task.getDeadlineMS(); // has activity been working too long? if ((this.started != -1) && (deadline > 0) && (this.started < (System.currentTimeMillis() - deadline))) return true; return false; } // if task has been doing work but not fast enough we may need to renew/review claim // will not work if you use less than 2 minutes for timeout public void reviewClaim() { // if not started, if completed or if hung then nothing to review if ((this.started == -1) || this.completed || this.isHung()) return; // once every 5 seconds we can renew a claim (might cause problems if run log is huge and we are tracking work back to the database) if (this.lastclaimed >= (System.currentTimeMillis() - 5000)) return; // otherwise there has been activity recently enough to warrant and update this.updateClaim(); } // return true if claimed or completed - false if canceled or timed out public boolean updateClaim() { if (this.task.isFromWorkQueue()) { // an incomplete load from work queue - edge error condition if (!this.task.hasAuditId()) { this.errorTr(191, this.task.getId()); return false; } // get the logs up to date as much as possible OperationResult res1 = Hub.instance.getWorkQueue().trackWork(this, false); // TODO add param for update claim? review this if (res1.hasErrors()) { this.errorTr(191, this.task.getId()); return false; } // try to extend our claim OperationResult res2 = Hub.instance.getWorkQueue().updateClaim(this.task); if (res2.hasErrors()) { this.errorTr(191, this.task.getId()); return false; } } this.lastclaimed = System.currentTimeMillis(); return true; } public void run() { try { OperationContext.set(this.opcontext); this.opcontext.setTaskRun(this); if (this.started == -1) { if (this.task.isFromWorkQueue()) this.infoTr(153, this.task.getId()); else this.traceTr(153, this.task.getId()); this.traceTr(144, Hub.instance.getWorkPool().getBucketOrDefault(this)); // if this is a queue task then mark it started if (this.task.isFromWorkQueue()) { FuncResult<String> k = Hub.instance.getWorkQueue().startWork(this.task.getWorkId()); if (k.hasErrors()) { // TODO replace with hub events Hub.instance.getWorkQueue().sendAlert(179, this.task.getId(), k.getMessage()); this.errorTr(179, this.task.getId(), k.getMessage()); this.complete(); return; } this.task.incCurrentTry(); this.task.withAuditId(k.getResult()); } RecordStruct params = this.task.getParams(); if (params == null) { params = new RecordStruct(); this.task.withParams(params); } // use temp folder unless skip flag if (this.task.isUsesTempFolder()) { try { File tempFolder = FileUtil.allocateTempFolder(); // needs to be canonical for log filtering params.setField("_TempFolder", tempFolder.getCanonicalPath()); } catch (Exception x) { this.errorTr(215, this, x); this.complete(); return; } } // the official "logger" is available via the _Logger special var IOperationLogger logger = this.opcontext.getLogger(); if (logger != null) params.setField("_Logger", logger); this.started = this.lastclaimed = System.currentTimeMillis(); // task start before work this.opcontext.fireEvent(OperationEvents.START_TASK, null); } // TODO review info feature DCTASKLOG in NCC // task might need some way to refer to info structures //params.setField("_Info", this.info.info); IWork work = this.task.getWork(); if (work == null) { this.errorTr(217, this); this.complete(); return; } work.run(this); if (work instanceof ISynchronousWork) this.complete(); } catch (Exception x) { this.errorTr(155, this.task.getId(), x); IWork work = this.task.getWorkInstance(); if (work != null) System.out.println("Work pool caught exception: " + work.getClass()); System.out.println("Stack Trace: "); x.printStackTrace(); this.complete(); } finally { //OperationContext.clear(); OperationContext.useHubContext(); } } public void resume() { if (this.opcontext != null) this.opcontext.touch(); Hub.instance.getWorkPool().submit(this); } public void kill(String msg) { this.completionlock.lock(); try { this.error(msg); this.kill(); } finally { this.completionlock.unlock(); } } /** * @param code code for message * @param msg message */ public void kill(long code, String msg) { this.completionlock.lock(); try { this.error(code, msg); this.kill(); } finally { this.completionlock.unlock(); } } public void exitTr(long code, Object... params) { this.completionlock.lock(); try { this.infoTr(code, params); } finally { this.completionlock.unlock(); } } public void kill() { this.completionlock.lock(); try { if (this.completed) return; OperationContext.set(this.opcontext); // collect inactive before error logging, logging updates the activity boolean inactive = this.isInactive(); this.errorTr(196, this.task); if (this.isOverdue()) this.errorTr(222, this.task); else if (inactive) this.errorTr(223, this.task); this.killed = true; IWork work = this.task.getWorkInstance(); if ((work != null) && (work instanceof ISmartWork)) try { ((ISmartWork)work).cancel(this); } catch (Exception x) { this.error("Error canceling task: " + x); } this.complete(); } finally { this.completionlock.unlock(); } } public void complete() { // make sure we complete in the correct context (only worker should call this method) OperationContext.set(this.opcontext); this.completionlock.lock(); try { // don't complete twice (but in try so we unlock) if (this.completed) return; this.completed = true; IWork work = this.task.getWorkInstance(); if ((work != null) && (work instanceof ISmartWork)) try { ((ISmartWork)work).completed(this); } catch (Exception x) { this.error("Error completing task: " + x); } // task observers could log still - so before close log this.opcontext.fireEvent(OperationEvents.COMPLETED, null); // task observers stop can/should no longer log this.opcontext.fireEvent(OperationEvents.STOP_TASK, null); // if this is a queue task then end it - only if we got an audit it though // TODO refine this - if we have a task id but not an audit id we should cleanup the queue... // TODO what should we do if not started - (this.started == -1) if (this.task.isFromWorkQueue() && this.task.hasAuditId()) { // don't go forward if this no longer holds a claim if (!this.updateClaim()) // record only that we ended but not a status or a queue change Hub.instance.getWorkQueue().trackWork(this, true); else if (this.hasErrors()) // record failure if errors Hub.instance.getWorkQueue().failWork(this); else // otherwise record completed Hub.instance.getWorkQueue().completeWork(this); } // don't remove temp folder till after record to queue in case the logger needs the folder to read log content from RecordStruct params = this.task.getParams(); if (params != null) { String tempFolder = params.getFieldAsString("_TempFolder"); if (StringUtil.isNotEmpty(tempFolder)) FileUtil.deleteDirectory(Paths.get(tempFolder)); } for (AutoCloseable ac : this.closeables) try { ac.close(); } catch (Exception x) { // ??? } this.closeables.clear(); // TODO what should we do if not started - (this.started == -1) if (this.task.isFromWorkQueue()) this.infoTr(154, this.getCode()); else this.traceTr(154, this.getCode()); // mark task completed Hub.instance.getWorkPool().complete(this); } finally { this.completionlock.unlock(); } } @Override public String toString() { return this.task.getTitle() + " (" + this.task.getId() + ")"; } public RecordStruct toStatusReport() { RecordStruct rec = new RecordStruct(); rec.setField("Id", this.task.getId()); rec.setField("Title", this.task.getTitle()); rec.setField("Tags", this.task.getTags()); rec.setField("Completed", this.completed); // TODO started, last touched/action, code, message, finished... return rec; } @Override public int hashCode() { return this.task.getTitle().hashCode(); } /* * For scripting calls - set the return value (convert to struct if not already) then call complete all at once * @param v */ public void returnValue(Object v) { this.value = Struct.objectToStruct(v); this.complete(); } public void returnEmpty() { this.complete(); } /* TODO supports groovy, enhance @Override public Object invokeMethod(String name, Object arg1) { // is really an object array Object[] args = (Object[])arg1; if ("return".equals(name)) { if (args.length > 0) this.returnValue(args[0]); else this.returnEmpty(); return null; } return super.invokeMethod(name, arg1); } */ public RecordStruct status() { RecordStruct status = this.task.status(); // TODO some of this may need review status.setField("Status", this.completed ? "Completed" : "Running"); status.setField("Start", new DateTime(this.started)); status.setField("End", null); status.setField("Hub", OperationContext.getHubId()); status.setField("Code", this.getCode()); status.setField("Message", this.getMessage()); status.setField("Log", this.getContext().getLog()); status.setField("Progress", this.opcontext.getProgressMessage()); status.setField("StepName", this.opcontext.getCurrentStepName()); status.setField("Completed", this.opcontext.getAmountCompleted()); status.setField("Step", this.opcontext.getCurrentStep()); status.setField("Steps", this.opcontext.getSteps()); return status; } public void addCloseable(AutoCloseable v) { this.closeables.add(v); } }