All Downloads are FREE. Search and download functionalities are using the official Maven repository.

brooklyn.util.task.BasicTask Maven / Gradle / Ivy

There is a newer version: 0.7.0-M1
Show newest version
package brooklyn.util.task;

import static brooklyn.util.JavaGroovyEquivalents.asString;
import static brooklyn.util.JavaGroovyEquivalents.elvisString;
import static brooklyn.util.JavaGroovyEquivalents.join;
import groovy.lang.Closure;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import brooklyn.management.ExecutionManager;
import brooklyn.management.Task;
import brooklyn.util.GroovyJavaMethods;
import brooklyn.util.exceptions.Exceptions;

import com.google.common.base.Throwables;

/**
 * The basic concrete implementation of a {@link Task} to be executed.
 *
 * A {@link Task} is a wrapper for an executable unit, such as a {@link Closure} or a {@link Runnable} or
 * {@link Callable} and will run in its own {@link Thread}.
 * 

* The task can be given an optional displayName and description in its constructor (as named * arguments in the first {@link Map} parameter). It is guaranteed to have {@link Object#notify()} called * once whenever the task starts running and once again when the task is about to complete. Due to * the way executors work it is ugly to guarantee notification after completion, so instead we * notify just before then expect the user to call {@link #get()} - which will throw errors if the underlying job * did so - or {@link #blockUntilEnded()} which will not throw errors. * * @see BasicTaskStub */ public class BasicTask extends BasicTaskStub implements Task { protected static final Logger log = LoggerFactory.getLogger(BasicTask.class); protected Callable job; public final String displayName; public final String description; protected final Set tags = new LinkedHashSet(); protected String blockingDetails = null; Object extraStatusText = null; /** * Constructor needed to prevent confusion in groovy stubs when looking for default constructor, * * The generics on {@link Closure} break it if that is first constructor. */ protected BasicTask() { this(Collections.emptyMap()); } protected BasicTask(Map flags) { this(flags, (Callable) null); } public BasicTask(Callable job) { this(Collections.emptyMap(), job); } public BasicTask(Map flags, Callable job) { this.job = job; if (flags.containsKey("tag")) tags.add(flags.remove("tag")); Object ftags = flags.remove("tags"); if (ftags!=null) { if (ftags instanceof Collection) tags.addAll((Collection)ftags); else { log.info("discouraged use of non-collection argument for 'tags' ("+ftags+") in "+this, new Throwable("trace of discouraged use of non-colleciton tags argument")); tags.add(ftags); } } description = elvisString(flags.remove("description"), ""); String d = asString(flags.remove("displayName")); if (d==null) d = join(tags, "-"); displayName = d; } public BasicTask(Runnable job) { this(GroovyJavaMethods.callableFromRunnable(job)); } public BasicTask(Map flags, Runnable job) { this(flags, GroovyJavaMethods.callableFromRunnable(job)); } public BasicTask(Closure job) { this(GroovyJavaMethods.callableFromClosure(job)); } public BasicTask(Map flags, Closure job) { this(flags, GroovyJavaMethods.callableFromClosure(job)); } @Override public String toString() { return "Task["+(displayName!=null && displayName.length()>0?displayName+ (tags!=null && !tags.isEmpty()?"":";")+" ":"")+ (tags!=null && !tags.isEmpty()?tags+"; ":"")+getId()+"]"; } // housekeeping -------------------- /* * These flags are set by BasicExecutionManager.submit. * * Order is guaranteed to be as shown below, in order of #. Within each # line it is currently in the order specified by commas but this is not guaranteed. * (The spaces between the # section indicate longer delays / logical separation ... it should be clear!) * * # submitter, submit time set, tags and other submit-time fields set, task tag-linked preprocessors onSubmit invoked * * # thread set, ThreadLocal getCurrentTask set * # start time set, isBegun is true * # task tag-linked preprocessors onStart invoked * # task end callback run, if supplied * * # task runs * * # task end callback run, if supplied * # task tag-linked preprocessors onEnd invoked (in reverse order of tags) * # end time set * # thread cleared, ThreadLocal getCurrentTask set * # Task.notifyAll() * # Task.get() (result.get()) available, Task.isDone is true * * Few _consumers_ should care, but internally we rely on this so that, for example, status is displayed correctly. * Tests should catch most things, but be careful if you change any of the above semantics. */ protected long submitTimeUtc = -1; protected long startTimeUtc = -1; protected long endTimeUtc = -1; protected Task submittedByTask; protected volatile Thread thread = null; private volatile boolean cancelled = false; protected volatile Future result = null; /** discouraged, but used in tests. not always set (e.g. if it is a scheduled task) * @deprecated in 0.4.0; use current execution context, as per CompoundTask.submitXxx */ @Deprecated protected ExecutionManager em; @SuppressWarnings("deprecation") void initExecutionManager(ExecutionManager em) { this.em = em; } synchronized void initResult(Future result) { if (this.result != null) throw new IllegalStateException("task "+this+" is being given a result twice"); this.result = result; notifyAll(); } // metadata accessors ------------ public Set getTags() { return Collections.unmodifiableSet(new LinkedHashSet(tags)); } public long getSubmitTimeUtc() { return submitTimeUtc; } public long getStartTimeUtc() { return startTimeUtc; } public long getEndTimeUtc() { return endTimeUtc; } public Future getResult() { return result; } public Task getSubmittedByTask() { return submittedByTask; } /** the thread where the task is running, if it is running */ public Thread getThread() { return thread; } // basic fields -------------------- public boolean isSubmitted() { return submitTimeUtc >= 0; } public boolean isBegun() { return startTimeUtc >= 0; } public synchronized boolean cancel() { return cancel(true); } public synchronized boolean cancel(boolean mayInterruptIfRunning) { if (isDone()) return false; boolean cancel = true; if (GroovyJavaMethods.truth(result)) { cancel = result.cancel(mayInterruptIfRunning); } cancelled = true; notifyAll(); return cancel; } public boolean isCancelled() { return cancelled || (result!=null && result.isCancelled()); } public boolean isDone() { return cancelled || (result!=null && result.isDone()); } /** * Returns true if the task has had an error. * * Only true if calling {@link #get()} will throw an exception when it completes (including cancel). * Implementations may set this true before completion if they have that insight, or * (the default) they may compute it lazily after completion (returning false before completion). */ public boolean isError() { if (!isDone()) return false; if (isCancelled()) return true; try { get(); return false; } catch (Throwable t) { return true; } } public T get() throws InterruptedException, ExecutionException { blockUntilStarted(); return result.get(); } public T getUnchecked() { try { return get(); } catch (InterruptedException e) { throw Exceptions.propagate(e); } catch (ExecutionException e) { throw Exceptions.propagate(e); } } // future value -------------------- public synchronized void blockUntilStarted() { while (true) { if (cancelled) throw new CancellationException(); if (result==null) try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Throwables.propagate(e); } if (result!=null) return; } } public void blockUntilEnded() { try { blockUntilStarted(); } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("call from "+Thread.currentThread()+" blocking until "+this+" finishes ended with error: "+t); /* contract is just to log errors at debug, otherwise do nothing */ return; } try { result.get(); } catch (Throwable t) { if (log.isDebugEnabled()) log.debug("call from "+Thread.currentThread()+" blocking until "+this+" finishes ended with error: "+t); /* contract is just to log errors at debug, otherwise do nothing */ return; } } public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { long start = System.currentTimeMillis(); long milliseconds = TimeUnit.MILLISECONDS.convert(timeout, unit); long end = start + milliseconds; while (end > System.currentTimeMillis()) { if (cancelled) throw new CancellationException(); if (result == null) wait(end - System.currentTimeMillis()); if (result != null) break; } long remaining = end - System.currentTimeMillis(); if (remaining > 0) { return result.get(remaining, TimeUnit.MILLISECONDS); } else { throw new TimeoutException(); } } /** * Returns a brief status string * * Plain-text format. Reported status if there is one, otherwise state which will be one of: *
    *
  • Not submitted *
  • Submitted for execution *
  • Ended by error *
  • Ended by cancellation *
  • Ended normally *
  • Running *
  • Waiting *
*/ public String getStatusSummary() { return getStatusString(0); } /** * Returns detailed status, suitable for a hover * * Plain-text format, with new-lines (and sometimes extra info) if multiline enabled. */ public String getStatusDetail(boolean multiline) { return getStatusString(multiline?2:1); } /** * This method is useful for callers to see the status of a task. * * Also for developers to see best practices for examining status fields etc * * @param verbosity 0 = brief, 1 = one-line with some detail, 2 = lots of detail */ protected String getStatusString(int verbosity) { // Thread t = getThread(); String rv; if (submitTimeUtc <= 0) rv = "Not submitted"; else if (!isCancelled() && startTimeUtc <= 0) { rv = "Submitted for execution"; if (verbosity>0) { long elapsed = System.currentTimeMillis() - submitTimeUtc; rv += " "+elapsed+" ms ago"; } if (verbosity >= 2 && getExtraStatusText()!=null) { rv += "\n\n"+getExtraStatusText(); } } else if (isDone()) { long elapsed = endTimeUtc - submitTimeUtc; String duration = ""+elapsed+" ms"; rv = "Ended "; if (isCancelled()) { rv += "by cancellation"; if (verbosity >= 1) rv+=" after "+duration; if (verbosity >= 2 && getExtraStatusText()!=null) { rv += "\n\n"+getExtraStatusText(); } } else if (isError()) { rv += "by error"; if (verbosity >= 1) { rv += " after "+duration; Object error; try { String rvx = ""+get(); error = "no error, return value "+rvx; /* shouldn't happen */ } catch (Throwable tt) { error = tt; } if (verbosity >= 2 && getExtraStatusText()!=null) { rv += "\n\n"+getExtraStatusText(); } //remove outer ExecException which is reported by the get(), we want the exception the task threw while (error instanceof ExecutionException) error = ((Throwable)error).getCause(); String errorMessage = null; if (error instanceof Throwable) errorMessage = ((Throwable)error).getMessage(); if (errorMessage==null || errorMessage.isEmpty()) errorMessage = ""+error; if (verbosity >= 1) rv += ": "+errorMessage; if (verbosity >= 2) { StringWriter sw = new StringWriter(); ((Throwable)error).printStackTrace(new PrintWriter(sw)); rv += "\n\n"+sw.getBuffer(); } } } else { rv += "normally"; if (verbosity>=1) { if (verbosity==1) { try { Object v = get(); rv += ", " +(v==null ? "no return value (null)" : "result: "+v); } catch (Exception e) { rv += ", but error accessing result ["+e+"]"; //shouldn't happen } } else { rv += " after "+duration; try { Object v = get(); rv += "\n\n" + (v==null ? "No return value (null)" : "Result: "+v); } catch (Exception e) { rv += " at first\n" + "Error accessing result ["+e+"]"; //shouldn't happen } if (verbosity >= 2 && getExtraStatusText()!=null) { rv += "\n\n"+getExtraStatusText(); } } } } } else { rv = getActiveTaskStatusString(verbosity); } return rv; } protected String getActiveTaskStatusString(int verbosity) { String rv = ""; Thread t = getThread(); // Normally, it's not possible for thread==null as we were started and not ended // However, there is a race where the task starts sand completes between the calls to getThread() // at the start of the method and this call to getThread(), so both return null even though // the intermediate checks returned started==true isDone()==false. if (t == null) { if (isDone()) { return getStatusString(verbosity); } else { //should only happen for repeating task which is not active return "Sleeping"; } } ThreadInfo ti = ManagementFactory.getThreadMXBean().getThreadInfo(t.getId(), (verbosity<=0 ? 0 : verbosity==1 ? 1 : Integer.MAX_VALUE)); if (getThread()==null) //thread might have moved on to a new task; if so, recompute (it should now say "done") return getStatusString(verbosity); if (verbosity >= 1 && GroovyJavaMethods.truth(blockingDetails)) { if (verbosity==1) // short status string will just show blocking details return blockingDetails; //otherwise show the blocking details, then a new line, then additional information rv = blockingDetails + "\n\n"; } if (verbosity>=2) { if (getExtraStatusText()!=null) { rv += getExtraStatusText()+"\n\n"; } rv += ""+toString()+"\n"; if (submittedByTask!=null) { rv += "Submitted by "+submittedByTask+"\n"; } if (this instanceof CompoundTask) { // list children tasks for compound tasks try { List childrenTasks = ((CompoundTask)this).getChildrenTasks(); if (!childrenTasks.isEmpty()) { rv += "Children:\n"; for (Task child: childrenTasks) { rv += " "+child+": "+child.getStatusDetail(false)+"\n"; } } } catch (ConcurrentModificationException exc) { rv += " (children not available - currently being modified)\n"; } } // // TODO spawned tasks would be interesting, but hard to retrieve // // as we store execution context in thread local for the _task_; // // it isn't actually stored on the task itself so not available to // // 3rd party threads calling this extended toString method rv += "\n"; } LockInfo lock = ti.getLockInfo(); if (!GroovyJavaMethods.truth(lock) && ti.getThreadState()==Thread.State.RUNNABLE) { //not blocked if (ti.isSuspended()) { // when does this happen? rv += "Waiting"; if (verbosity >= 1) rv += ", thread suspended"; } else { rv += "Running"; if (verbosity >= 1) rv += " ("+ti.getThreadState()+")"; } } else { rv += "Waiting"; if (verbosity>=1) { if (ti.getThreadState() == Thread.State.BLOCKED) { rv += " (mutex) on "+lookup(lock); //TODO could say who holds it } else if (ti.getThreadState() == Thread.State.WAITING) { rv += " (notify) on "+lookup(lock); } else if (ti.getThreadState() == Thread.State.TIMED_WAITING) { rv += " (timed) on "+lookup(lock); } else { rv = " ("+ti.getThreadState()+") on "+lookup(lock); } } } if (verbosity>=2) { StackTraceElement[] st = ti.getStackTrace(); st = StackTraceSimplifier.cleanStackTrace(st); if (st!=null && st.length>0) rv += "\n" +"At: "+st[0]; for (int ii=1; ii