Execution Context

Jakob Jenkov
Last update: 2014-06-23

An execution context is an alternative or supplement to the AppException and ErrorInfo described in the exception handling template.

As shown in the text on the AppException you can collect context information in the AppException as it is propagated up towards the top of the application. This context information is stored internally in the ErrorInfo list.

In this text I will show you an alternative, or a supplement if you will, called an "Execution Context".

An execution context is an object attached to the current thread of execution. For instance, by storing the execution context object in a ThreadLocal variable, or by mapping it to the thread in a statically available Map.

At any level in your application you can thus write execution context information into the execution context object. You can do this as the execution flows down, as illustrated here:

Execution Flow - with calls to an Execution Context
Execution Flow - with calls to an Execution Context.

You can also write to the execution context as an exception is propagated up the call stack, as illustrated here:

Exception Propagation Flow - with calls to an Execution Context
Exception Propagation Flow - with calls to an Execution Context.

As you can imagine, the execution context object can contain execution context information, regardless of whether a request succeeded, or failed. You can think of the execution context object as an "execution log". Whether a request failed or succeeded, this execution log contains all information related to that request processing, provided your application writes that information to it, that is.

When a request is successfully processed, you can write the whole execution context object to a log file, as a single, coherent structure (e.g. XML). Or, you can just ignore it, and clear the context.

Similarly, when a request fails you can write the complete execution context object to a log file, along with any relevant information contained in the exception.

Benefits

The benefit of using an execution context together with the AppException is that you can get more information about the execution, than you can with the AppException alone.

The extra information about the execution may not enable you to better determine type and severity of an exception. But, when logging the error, you could log the execution context too. It might be helpful for the developers, when trying to understand why the exception occurred.

Drawbacks

A drawback (disadvantage) of using an execution context could be, that your code gets cluttered with loads and loads of small calls to the execution context. Additionally, there is no guarantee that the logged information actually helps developers diagnose the error. It all depends on how you use the execution context, and what information you include in your calls.

Simple Execution Context Implementation

Let's look at how you could implement a simple execution context. It's implemented as a single class called ExecutionContext, which keeps all calls to it internally in a list. The execution path is thus flat. You cannot see the real tree-like structure the execution path follows through your code.

public class ExecutionContext {

  protected String contextId  = null;
  protected String locationId = null;
  protected Object details    = null;

  protected static Map<Thread, List<ExecutionContext>> executionContexts =
    new ConcurrentHashMap<Thread, List<ExecutionContext>>();


  public static void log(String contextId, String locationId,
      Object details){

    List<ExecutionContext> executionContextListForThread =
          getExecutionContext();

    ExecutionContext executionContext = new ExecutionContext();
    executionContext.contextId  = contextId;
    executionContext.locationId = locationId;
    executionContext.details    = details;

    executionContextListForThread.add(executionContext);
  }


  public static void clearExecutionContext() {
    getExecutionContext().clear();
  }


  public static List<ExecutionContext> getExecutionContext() {

    List<ExecutionContext> executionContextListForThread =
            executionContexts.get(Thread.currentThread());

    if(executionContextListForThread == null){

        executionContextListForThread =
            new ArrayList<ExecutionContext>();

        executionContexts.put(Thread.currentThread(),
              executionContextListForThread);
    }

    return executionContextListForThread;
  }
}

Here is a simple example of how to use it:

public class ExecutionContextExample {


  public static void main(String[] args) {

    ExecutionContext.log("ExecutionContextExample", "main", null);

    try {
      level1();
    } catch(AppException e){
      log(e);
      log(ExecutionContext.getExecutionContext());
    }

  }

  private static void level1() throws AppException {
    ExecutionContext.log("ExecutionContextExample", "level1", null);
    level2();
  }

  private static void level2() throws AppException{
    try {

      ExecutionContext.log("ExecutionContextExample", "level2-1", null);
      level3();
      ExecutionContext.log("ExecutionContextExample", "level2-2", null);

    } catch (Throwable t){

      AppException appException = new AppException();
      ErrorInfo errorInfo = appException.addInfo();
      errorInfo.setCause(t);

      //... fill more data into ErrorInfo object.

     ExecutionContext.log("ExecutionContextExample", "level-2-error",
         errorInfo);

      throw appException;

    }
  }

  private static void level3() throws Exception {
    ExecutionContext.log("ExecutionContextExample", "main", null);
  }

  private static void log(List<ExecutionContext> executionContext) {
    //log execution context list to file. Perhaps as an XML structure.
  }

  private static void log(AppException e) {
    //log AppException to file. Perhaps as an XML structure.
  }


}

Notice how each method ( level1() to level3() ) calls the ExecutionContext. Also notice how the ExecutionContext list is now logged, in case an exception occurs.

Advanced Execution Context Implementation

The previous implementation of the ExecutionContext only keeps a flat list of the execution context information written to it. The execution path is really a tree structure, and not a flat list. Therefore, I have developed an ExecutionContextTree class, which can contain this information.

In order to collect the execution tree path correctly, you must now use two methods instead of one:

ExecutionContextTree.pre("contextId", "locationId", null);

ExecutionContextTree.post();

The pre() call creates a new node, and attaches it to the parent node (if any). Any calls to pre() after this one, will result in new nodes being attached to the newly created node.

The post() call removes the node as the current parent in the execution tree. The next call to pre() will now attach a node to the parent of the node just removed as parent node.

Here is the code:

public class ExecutionContextTree {

    public static class ExecutionContextNode {

        public String contextId  = null;
        public String locationId = null;
        public Object details    = null;

        public ExecutionContextNode   parent   = null;
        public List<ExecutionContextNode> children =
            new ArrayList<ExecutionContextNode>();
    }


    protected static Map<Thread, ExecutionContextNode> roots          =
        new ConcurrentHashMap <Thread,ExecutionContextNode>();

    protected static Map<Thread, ExecutionContextNode> currentParents =
        new ConcurrentHashMap<Thread, ExecutionContextNode>();


    public static void pre(String contextId, String locationId,
            Object details){

        Thread currentThread = Thread.currentThread();
        ExecutionContextNode node = new ExecutionContextNode();
        if(roots.get(currentThread) == null) {
            roots.put(currentThread, node);
        }

        node.contextId  = contextId;
        node.locationId = locationId;
        node.details    = details;

        node.parent = currentParents.get(currentThread);
        if(node.parent != null){
            node.parent.children.add(node);
        }
        currentParents.put(currentThread, node);
    }

    public static void post(){
        Thread currentThread = Thread.currentThread();
        ExecutionContextNode node = currentParents.get(currentThread);
        if(node.parent != null){
            currentParents.put(currentThread, node.parent);
        } else {
            //remove top node from currentParents and from root,
            // root was removed so there are no more parents or root.
            currentParents.remove(currentThread);
            roots.remove(currentThread);
        }
    }

    public static ExecutionContextNode root() {
        return roots.get(Thread.currentThread());
    }

    public static void clear() {
        roots.remove(Thread.currentThread());
        currentParents.remove(Thread.currentThread());
    }

}

Here is an example of how to use this ExecutionContextTree:

public class ExecutionContextTreeExample {

  public static void main(String[] args) {
    try {
      ExecutionContextTree.pre("ExecutionContextExample", "main", null);
      level1();
      ExecutionContextTree.post();

      ExecutionContextTree.clear();

    } catch(AppException e){
      log(e);
      log(ExecutionContextTree.root());
    }
      log(ExecutionContextTree.root());
    }

  private static void level1() throws AppException {
    ExecutionContextTree.pre("ExecutionContextExample", "level1", null);
    level2();
    ExecutionContextTree.post();
  }

  private static void level2() throws AppException{
    try {
      ExecutionContextTree.pre("ExecutionContextExample", "level2-1",
            null);

      level3();
      ExecutionContextTree.post();

    } catch (Throwable t){

      AppException appException = new AppException();
      ErrorInfo errorInfo = appException.addInfo();
      errorInfo.setCause(t);

      //... fill more data into ErrorInfo object.

      ExecutionContextTree.pre("ExecutionContextExample", "level-2-error",
            errorInfo);
      ExecutionContextTree.post();

      throw appException;
    }
  }

  private static void level3() throws Exception {
    ExecutionContextTree.pre("ExecutionContextExample", "Level-3", null);
    ExecutionContextTree.post();
  }

  private static void log(AppException e) {
    //log AppException to file. Perhaps as an XML structure.
  }

  private static void log(
    ExecutionContextTree.ExecutionContextNode executionContextRoot) {

    //log execution context list to file. Perhaps as an XMl structure.

    ExecutionContextTree.ExecutionContextNode node =
          ExecutionContextTree.root();

  }

}

Notice how pre() and post() calls are paired.

Also notice how the ExecutionContextTree is cleared if no exception occurs, and logged if an exception occurs.

Insert pre() and post() calls using AOP

As you can see, the pre() and post() calls are very often insert at the start and end of a method call. This would be a lot of work to do inside every method if you had to do it manually. Luckily, this kind of task is what we have Aspect Oriented Programming for (AOP).

Keep in mind though, that you may not need the entire context tree to be available for your debugging. Use the ExecutionContextTree with consideration. Collect the information that makes sense, and leave out some of the smaller detail.

Of course it can be hard to predict exactly what detail becomes important during debugging, as you don't know exactly which errors will occur, ahead of time. As you are debugging, you may need to add more calls to the ExecutionContextTree to capture all the information you need.

Jakob Jenkov

Featured Videos

Java Generics

Java ForkJoinPool

P2P Networks Introduction



















Close TOC
All Tutorial Trails
All Trails
Table of contents (TOC) for this tutorial trail
Trail TOC
Table of contents (TOC) for this tutorial
Page TOC
Previous tutorial in this tutorial trail
Previous
Next tutorial in this tutorial trail
Next