Execution Context
Jakob Jenkov |
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. |
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. |
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.
Tweet | |
Jakob Jenkov |