7 Replies Latest reply on Jul 11, 2012 3:01 AM by vsevel

    Can a Byteman helper throw an Exception?

    spennec

      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?
          adinn

          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

           

            1. 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
            2. perform side effects at the trigger point and then return immediately from the trigger method frame to its caller
            3. 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

           

            1. one or more Java expressions
            2. zero or more Java expressions then a terminating RETURN expression
            3. zero or more Java expressions then a terminating THROW expression

           

          Rules are subject to two further restrictions:

           

          1. the expression (if any) provided after the RETURN keyword needs to have the same type as the trigger method return type
          2. 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?
            spennec

            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?
              adinn

              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?
                spennec

                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?
                  vsevel

                  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.

                   

                  one other way of doing (almost) this is to define 2 rules: AT THROW and AT EXIT. with that we cover the cases where we return, and the case where an exception is thrown directly from the onMessage method. however, we can't trap the case we propagate up the stack an exception that is been thrown by a method called by the onMessage method.

                   

                  so in the end, I do not see a way of an AOP around invoke in byteman that works when:

                  - there are no exceptions

                  - an exception is thrown by the onMessage method

                  - an exception is thrown by a method called by the onMessage method

                   

                  regards,

                  vince

                  • 6. Re: Can a Byteman helper throw an Exception?
                    adinn

                    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:

                     

                     

                    one other way of doing (almost) this is to define 2 rules: AT THROW and AT EXIT. with that we cover the cases where we return, and the case where an exception is thrown directly from the onMessage method. however, we can't trap the case we propagate up the stack an exception that is been thrown by a method called by the onMessage method.

                     

                    so in the end, I do not see a way of an AOP around invoke in byteman that works when:

                    - there are no exceptions

                    - an exception is thrown by the onMessage method

                    - an exception is thrown by a method called by the onMessage method

                     

                    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?
                      vsevel

                      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