Can a Byteman helper throw an Exception?Sebastien Pennec Jan 30, 2012 4:58 AM
Hello,
I'd like to make a rule with byteman 1.6.0 that intercepts all calls to a certain method. It would then do something like:
//doSomething try { originalObject.doOriginalCall() } catch (Throwable t) { logger.error("Something went wrong", e); throw t; }
It would be "around" the original call, do something before the call, then let the original call do its job and, only if something goes wrong, log the problem and rethrow the exception so that the code around the intercepted method works as it is supposed to.
Unfortunately, in that situation if I rethrow the exception, it is warpped in a byteman Exception:
08:58:25,650 INFO [STDOUT] MyRuleName : org.jboss.byteman.rule.exception.ExecuteException: MethodExpression.interpret : exception invoking method intercept file default.btm line 91.
org.jboss.byteman.rule.exception.ExecuteException: MethodExpression.interpret : exception invoking method intercept file default.btm line 91
at org.jboss.byteman.rule.expression.MethodExpression.interpret(MethodExpression.java:321)
at org.jboss.byteman.rule.Action.interpret(Action.java:144)
What am I doing wrong?
Thanks for your help!
Sébastien
1. Re: Can a Byteman helper throw an Exception?Andrew Dinn Jan 30, 2012 6:26 AM (in response to Sebastien Pennec)
Hi Sebastien,
I am glad you asked this as it is a very interesting question.
The short answer is: no a helper class cannot throw an exception.
The tricky answer is: yes a helper class can throw an exception.
Before I provide that answer here is the long answer: why a helper class cannot throw an exception.
Byteman really only supports 3 sorts of control flow
- perform side effects at the trigger point (the location in the method which triggers the rule i.e. where the rule code gets injected) and then continue execution from the trigger point
- perform side effects at the trigger point and then return immediately from the trigger method frame to its caller
- perform side effects at the trigger point and then throw an exception immediately from the trigger method frame to its caller
These correspond to rules where the DO part contains
- one or more Java expressions
- zero or more Java expressions then a terminating RETURN expression
- zero or more Java expressions then a terminating THROW expression
Rules are subject to two further restrictions:
- the expression (if any) provided after the RETURN keyword needs to have the same type as the trigger method return type
- the expression which follows the THROW needs to have a type which is declared as a checked exception by the trigger method or alternatively is a RuntimeException or Error
These two restrictions ensure that injected code can only change behaviour internal to the trigger method but cannot break the method's contract with its callers.
In particular, if any of the Java expressions (or indeed the expression following the RETURN or THROW keyword) throws an exception then Byteman will quite deliberately rethrow the exception wrapped in an ExecuteException, the latter being a subtype of RuntimeException. This is because it cannot know whether the original exception is a legitimate thing for the trigger method to throw. The only type-safe behaviour is to throw an exception that callers of the trigger method might expect to see (i.e. a RuntimeException) but to throw it as a special exception type which has no meaning that leads the application into thinking it can ahndle the problem.
Ok, so now what about the tricky answer?
Well, the way the Byteman rule engine implements RETURN and THROW is by throwing a specific type of exception, either a ReturnException or a ThrowException. These are both subtypes of ExecuteException. A ReturnException includes an Object field which is the value to be returned from the trigger method. A ThrowException inlcudes a Throwable field which is the exception to be thrown from the trigger method.
All rule code injected into a trigger method is protected as a try region with catch blocks for each of these three possible exceptions, ReturnException, ThrowException and ExceuteException. The catch block for return exception unwraps the return value embedded in the ReturnException stacks it, casts it to the correct vale and and then executes the appropriate type of RET instruction (obviously for a void method it just returns). The catch block for a ThrowException unwraps the Throwable, casts it to an expected type and then executes a THROW instruction. The catch block for an ExecuteException just rethrows the exception.
So, your helper method could hijack this mechanism to throw the embedded exception as follows:
RULE redirect calls to myMethod
CLASS MyClass
METHOD myMethod(MyArgType1, MyArgType2)
HELPER MyHelper
AT ENTRY
IF TRUE
DO setRecursiveTriggering(false);
callMyMethod($0, $1, $2)
ENDRULE
Note that the call to setRecursiveTriggering is needed to ensure that the rule does not trigger repeatedly when myMethod is invoked inside the helper method leading to a stack overflow. Also, this version of the rule assumes that myMethod is void. If it has a return value then the last line in the DO clause would need to be
RETURN callMyMethod($0, $1, $2).
The helper method woudl
class MyHelper
{
public void callMyMethod(MyClass target, MyArgType1 arg1, MyArgType2 arg2)
{
try {
target.myMethod(arg1, arg2);
} catch (Throwable e) {
log(e);
ThrowException te = new ThrowException(e);
throw te;
}
}
}
Once again this assumes myMethod has void type. If it returns a value then you need to change the return type of callMyMethod and insert return before the recursive call.
Will this work? Yes, with the current implementation it will and, indeed, this implementation is unlikely to change but I cannot give any absolute guarantees.
Is this (type)safe to use? Well, it is if you ensure that the exceptions you embed into the ThrowException are checked exceptions delcared by myMethod or are runtime exceptions or errors. Obviously, with the code above this is true but clearly you could modify your helper so it embeds an exception with an unexpected type. If you do that's your problem :-)
Anyway, if you do try this let me know how it works out. Also, I'd be interested to hear what you are using Byteman for.
2. Re: Can a Byteman helper throw an Exception?Sebastien Pennec Jan 30, 2012 7:24 AM (in response to Andrew Dinn)
Hi Andrew,
Thanks for replying so quickly, and with so much details! :-)
I think that the documentation is missing a few lines about this... your post might be the perfect explanation to add to the existing doc.
I'm testing a version of my Helper that throws a ThrowException instance, and everything seems to be fine... mostly because the helper can only catch RuntimeException subclasses at this point. Here are the details:
We are using JBoss 5.x, and a lot of our applications rely on JMS. The goal is to intercept calls to MessageListener implementations, at the onMessage() method and do the following:
- Set log4j's MDC with the JMSMessageID and CorrelationID
- Log the JMS message's content
- Delegate the call to the actual MessageListener's implementation
- If everything goes fine, just clean the MDC in a finally block
- If something goes wrong, log an error, along with the stacktrace while the logs are still linked to the JMSMessageID and CorrelationID, and then rethrow the exception so that everything goes as if there was no byteman involved.
Since there are no checked exceptions in the signature of the onMessage method, we should be safe throwing a ThrowException instance.
For the sake of completeness... Here is the rule definition:
RULE Intercept HornetQ message processing
INTERFACE javax.jms.MessageListener
METHOD void onMessage(javax.jms.Message)
HELPER com.company.util.byteman.OnMessageHelper
BIND
messageListener:MessageListener = $0;
message:Message = $1
IF callerCheck("com.company.*.onMessage", true, true, true, 0, 1)
DO intercept(messageListener, message);return
ENDRULE
And the helper code:
public void intercept(MessageListener listener, Message message) throws Throwable {
// disable recursive firing
setTriggering(false);
MDC.put(JMS_MESSAGE_ID, getMessageID(message));
MDC.put(JMS_CORRELATION_ID, getCorrelationID(message));
if (logger.isDebugEnabled()) {
logMessageBeforeInvocation(message);
}
try {
listener.onMessage(message);
} catch (Throwable t) {
logger.error("An error occured when handling message: " + toText(message), t);
throw new ThrowException(t);
} finally {
MDC.remove(JMS_MESSAGE_ID);
MDC.remove(JMS_CORRELATION_ID);
}
}
Hope you find this interesting :-)
Anyway thanks again for the answer!
3. Re: Can a Byteman helper throw an Exception?Andrew Dinn Jan 30, 2012 7:30 AM (in response to Sebastien Pennec)
Hi Sebastien,
Sebastien Pennec wrote:
Hope you find this interesting :-)
Anyway thanks again for the answer!
Yes, that's very interesting and thanks for explaining the context for your question. It's very nice that Byteman can do this and I am sure there are lots of other people who would be interested in using the same trick. I think I may have to blog about this :-)
4. Re: Can a Byteman helper throw an Exception?Sebastien Pennec Jan 30, 2012 7:49 AM (in response to Andrew Dinn)
Sure just let me know when you blog about this :-)
One more thing: why did we use Byteman instead of AOP for this? Because JBoss EAP jars are signed, and AOP poses problem with signed jars.
5. Re: Can a Byteman helper throw an Exception?Vincent Sevel Jul 10, 2012 10:36 AM (in response to Sebastien Pennec)
Hi, the issue with the code snippet is that by doing a setTriggering(false); you remove the ability to trigger other rules down the line in listener.onMessage(message); and any other methods called by the onMessage method.
regards,
vince
6. Re: Can a Byteman helper throw an Exception?Andrew Dinn Jul 10, 2012 11:07 AM (in response to Vincent Sevel)
Vincent Sevel wrote:
Hi, the issue with the code snippet is that by doing a setTriggering(false); you remove the ability to trigger other rules down the line in listener.onMessage(message); and any other methods called by the onMessage method.
Well, you only stop methods which are triggered below calls amde form the rule body. Calls in other threads are unaffected as are calls whcih occur in the triggering thread after the rule has completed executing. You also remove the ability to enter an infinite triggering loop, which is quite a good thing to have :-)
Vincent Sevel wrote:
No, that's not possible with Byteman. But then again, Byteman is not really intended to be used for doing this sort of thing. Byteman concentrates on providing targetted, dynamic transformations which are quick to implement, don't require a compile/deploy cycle and can be installed and uninstalled with low overhead. That's why so far I have resisted putting in features like wild-cards/patterns in CLASS and METHOD clauses which woudl be more appropriate for the sort of audience targetted by AOP systems. If you want to do wholesale program transformations, say to provide variant program semantics, then use a full-blown AOP system (preferably with offline transformations to ensure it does not run like a dog). They are very good for that sort of thing. Byteman isn't the right tool for that sort of job.
7. Re: Can a Byteman helper throw an Exception?Vincent Sevel Jul 11, 2012 3:01 AM (in response to Andrew Dinn)
Hi, something like this dows work:
RULE trace myrule
CLASS MyPojo1
METHOD toto1
HELPER com.lodh.arte.logserver.byteman.MyHelper
IF needIntercepting()
DO intercept($0, $1, $2); RETURN
ENDRULE
public class MyHelper extends Helper {
static Map<Thread, Boolean> stacks = new ConcurrentHashMap<Thread, Boolean>();
protected MyHelper(Rule rule) {
super(rule);
}
public boolean needIntercepting() {
return !stacks.containsKey(Thread.currentThread());
}
public void intercept(MyPojo1 pojo2, boolean ex2, boolean ex3) {
Thread key = Thread.currentThread();
try {
stacks.put(key, true);
System.out.println("intercept before");
pojo2.toto1(ex2, ex3);
System.out.println("intercept after");
} catch (Throwable t) {
System.out.println("intercept ex: " + t);
throw new ThrowException(t);
} finally {
stacks.remove(key);
}
}
}
public class MyPojo1 {
public void toto1(boolean ex1, boolean ex2) {
System.out.println("toto1 - 1");
if (ex1) throw new RuntimeException("my ex1");
new MyPojo2().toto2(ex2);
System.out.println("toto1 - 2");
}
}
public class MyPojo2 {
public void toto2(boolean ex) {
System.out.println("toto2 - 1");
if(ex) throw new RuntimeException("my ex2");
System.out.println("toto2 - 2");
}
}
this only handles activating myrule once in a given thread execution (this could be improved), but all other rules will get activated down the line.
thanks,
vince