Coverage Report - org.mule.routing.EventCorrelator
 
Classes in this File Line Coverage Branch Coverage Complexity
EventCorrelator
42%
58/137
30%
21/70
3.944
EventCorrelator$1
0%
0/22
0%
0/8
3.944
 
 1  
 /*
 2  
  * $Id: EventCorrelator.java 11567 2008-04-11 13:08:05Z dirk.olmes $
 3  
  * --------------------------------------------------------------------------------------
 4  
  * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.com
 5  
  *
 6  
  * The software in this package is published under the terms of the CPAL v1.0
 7  
  * license, a copy of which has been included with this distribution in the
 8  
  * LICENSE.txt file.
 9  
  */
 10  
 package org.mule.routing;
 11  
 
 12  
 import org.mule.api.MuleContext;
 13  
 import org.mule.api.MuleEvent;
 14  
 import org.mule.api.MuleMessage;
 15  
 import org.mule.api.routing.MessageInfoMapping;
 16  
 import org.mule.api.routing.ResponseTimeoutException;
 17  
 import org.mule.api.routing.RoutingException;
 18  
 import org.mule.config.i18n.CoreMessages;
 19  
 import org.mule.context.notification.RoutingNotification;
 20  
 import org.mule.routing.inbound.EventGroup;
 21  
 import org.mule.util.MapUtils;
 22  
 import org.mule.util.concurrent.Latch;
 23  
 
 24  
 import java.util.ArrayList;
 25  
 import java.util.Collections;
 26  
 import java.util.Iterator;
 27  
 import java.util.List;
 28  
 import java.util.Map;
 29  
 
 30  
 import javax.resource.spi.work.Work;
 31  
 import javax.resource.spi.work.WorkException;
 32  
 
 33  
 import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
 34  
 import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap;
 35  
 import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
 36  
 import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean;
 37  
 
 38  
 import org.apache.commons.collections.buffer.BoundedFifoBuffer;
 39  
 import org.apache.commons.logging.Log;
 40  
 import org.apache.commons.logging.LogFactory;
 41  
 
 42  
 /**
 43  
  * TODO
 44  
  */
 45  0
 public class EventCorrelator
 46  
 {
 47  
     /**
 48  
      * logger used by this class
 49  
      */
 50  2
     protected transient final Log logger = LogFactory.getLog(EventCorrelator.class);
 51  
 
 52  
     public static final String NO_CORRELATION_ID = "no-id";
 53  
     
 54  
     public static final int MAX_PROCESSED_GROUPS = 50000;
 55  
     /**
 56  
      * A map of EventGroup objects. These represent one or more messages to be
 57  
      * agregated, keyed by message id. There will be one response message for every
 58  
      * EventGroup.
 59  
      */
 60  2
     protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
 61  
 
 62  
     /**
 63  
      * A map of locks used to wait for response messages for a given message id
 64  
      */
 65  2
     protected final ConcurrentMap locks = new ConcurrentHashMap();
 66  
 
 67  
     /**
 68  
      * The collection of messages that are ready to be returned to the callee. Keyed
 69  
      * by Message ID
 70  
      */
 71  2
     protected final ConcurrentMap responseMessages = new ConcurrentHashMap();
 72  
 
 73  2
     protected final BoundedFifoBuffer processedGroups = new BoundedFifoBuffer(MAX_PROCESSED_GROUPS);
 74  
 
 75  2
     private int timeout = -1; // undefined
 76  
 
 77  2
     private boolean failOnTimeout = true;
 78  
 
 79  
     private MessageInfoMapping messageInfoMapping;
 80  
 
 81  
     private MuleContext context;
 82  
 
 83  
     private EventCorrelatorCallback callback;
 84  
 
 85  2
     private AtomicBoolean timerStarted = new AtomicBoolean(false);
 86  
 
 87  
 
 88  
     public EventCorrelator(EventCorrelatorCallback callback, MessageInfoMapping messageInfoMapping, MuleContext context)
 89  2
     {
 90  2
         if (callback == null)
 91  
         {
 92  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("EventCorrelatorCallback").getMessage());
 93  
         }
 94  2
         if (messageInfoMapping == null)
 95  
         {
 96  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("MessageInfoMapping").getMessage());
 97  
         }
 98  2
         if (context == null)
 99  
         {
 100  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("MuleContext").getMessage());
 101  
         }
 102  2
         this.callback = callback;
 103  2
         this.messageInfoMapping = messageInfoMapping;
 104  2
         this.context = context;
 105  
 
 106  
 
 107  2
     }
 108  
 
 109  
     public void enableTimeoutMonitor() throws WorkException
 110  
     {
 111  0
         if (!timerStarted.get())
 112  
         {
 113  
 
 114  0
             this.context.getWorkManager().scheduleWork(new Work()
 115  
             {
 116  
                 public void release()
 117  
                 {
 118  
                     //no op
 119  0
                 }
 120  
 
 121  
 
 122  0
                 public void run()
 123  
                 {
 124  
                     while (true)
 125  
                     {
 126  0
                         List expired = new ArrayList(1);
 127  0
                         for (Iterator iterator = eventGroups.values().iterator(); iterator.hasNext();)
 128  
                         {
 129  0
                             EventGroup group = (EventGroup) iterator.next();
 130  0
                             if ((group.getCreated() + getTimeout()) < System.currentTimeMillis())
 131  
                             {
 132  0
                                 expired.add(group);
 133  
                             }
 134  0
                         }
 135  0
                         if (expired.size() > 0)
 136  
                         {
 137  0
                             for (Iterator iterator = expired.iterator(); iterator.hasNext();)
 138  
                             {
 139  0
                                 EventGroup group = (EventGroup) iterator.next();
 140  0
                                 eventGroups.remove(group.getGroupId());
 141  0
                                 locks.remove(group.getGroupId());
 142  
 
 143  0
                                 context.fireNotification(new RoutingNotification(group.toMessageCollection(), null,
 144  
                                         RoutingNotification.CORRELATION_TIMEOUT));
 145  
 
 146  
 //                            if(isFailOnTimeout())
 147  
 //                            {
 148  0
                                 group.toArray()[0].getService().getExceptionListener().exceptionThrown(
 149  
                                         new CorrelationTimeoutException(CoreMessages.correlationTimedOut(group.getGroupId()),
 150  
                                                 group.toMessageCollection()));
 151  
 //                            }
 152  
 //                            else
 153  
 //                            {
 154  
 //                                //We could invoke a callback on the compoennt here or just dispatch the events??
 155  
 //                            }
 156  0
                             }
 157  
                         }
 158  
                         try
 159  
                         {
 160  0
                             Thread.sleep(100);
 161  
                         }
 162  0
                         catch (InterruptedException e)
 163  
                         {
 164  0
                             break;
 165  0
                         }
 166  0
                     }
 167  0
                 }
 168  
             }
 169  
 
 170  
             );
 171  
         }
 172  0
     }
 173  
 
 174  
     /**
 175  
      * @return
 176  
      * @deprecated this is used by a test, but I would like to remove this method
 177  
      */
 178  
     public Map getResponseMessages()
 179  
     {
 180  0
         return Collections.unmodifiableMap(responseMessages);
 181  
     }
 182  
 
 183  
     public MuleMessage process(MuleEvent event) throws RoutingException
 184  
     {
 185  6
         addEvent(event);
 186  6
         Object correlationId = messageInfoMapping.getCorrelationId(event.getMessage());
 187  6
         if (locks.get(correlationId) != null)
 188  
         {
 189  2
             locks.remove(correlationId);
 190  2
             return (MuleMessage) responseMessages.remove(correlationId);
 191  
         }
 192  
         else
 193  
         {
 194  4
             return null;
 195  
         }
 196  
     }
 197  
 
 198  
     public void addEvent(MuleEvent event) throws RoutingException
 199  
     {
 200  
 // the correlationId of the event's message
 201  6
         final Object groupId = messageInfoMapping.getCorrelationId(event.getMessage());
 202  
 
 203  6
         if (groupId == null || groupId.equals("-1"))
 204  
         {
 205  0
             throw new RoutingException(CoreMessages.noCorrelationId(), event.getMessage(), event
 206  
                     .getEndpoint());
 207  
         }
 208  
 
 209  
         // indicates interleaved EventGroup removal (very rare)
 210  6
         boolean lookupMiss = false;
 211  
 
 212  
 // spinloop for the EventGroup lookup
 213  
         while (true)
 214  
         {
 215  6
             if (lookupMiss)
 216  
             {
 217  
                 try
 218  
                 {
 219  
                     // recommended over Thread.yield()
 220  0
                     Thread.sleep(1);
 221  
                 }
 222  0
                 catch (InterruptedException interrupted)
 223  
                 {
 224  0
                     Thread.currentThread().interrupt();
 225  0
                 }
 226  
             }
 227  
 
 228  6
             if (isGroupAlreadyProcessed(groupId))
 229  
             {
 230  0
                 if (logger.isDebugEnabled())
 231  
                 {
 232  0
                     logger.debug("An event was received for an event group that has already been processed, " +
 233  
                             "this is probably because the async-reply timed out. Correlation Id is: " + groupId +
 234  
                             ". Dropping event");
 235  
                 }
 236  
                 //Fire a notification to say we received this message
 237  0
                 context.fireNotification(new RoutingNotification(event.getMessage(),
 238  
                         event.getEndpoint().getEndpointURI().toString(),
 239  
                         RoutingNotification.MISSED_ASYNC_REPLY));
 240  0
                 return;
 241  
             }
 242  
             // check for an existing group first
 243  6
             EventGroup group = this.getEventGroup(groupId);
 244  
 
 245  
 // does the group exist?
 246  6
             if (group == null)
 247  
             {
 248  
                 // ..apparently not, so create a new one & add it
 249  2
                 group = this.addEventGroup(callback.createEventGroup(event, groupId));
 250  
             }
 251  
 
 252  
 // ensure that only one thread at a time evaluates this EventGroup
 253  6
             synchronized (group)
 254  
             {
 255  
                 // make sure no other thread removed the group in the meantime
 256  6
                 if (group != this.getEventGroup(groupId))
 257  
                 {
 258  
                     // if that is the (rare) case, spin
 259  0
                     lookupMiss = true;
 260  0
                     continue;
 261  
                 }
 262  
 
 263  6
                 if (logger.isDebugEnabled())
 264  
                 {
 265  0
                     logger.debug("Adding event to response aggregator group: " + groupId);
 266  
                 }
 267  
 
 268  
                 // add the incoming event to the group
 269  6
                 group.addEvent(event);
 270  
 
 271  
 // check to see if the event group is ready to be aggregated
 272  6
                 if (callback.shouldAggregateEvents(group))
 273  
                 {
 274  
                     // create the response message
 275  2
                     MuleMessage returnMessage = callback.aggregateEvents(group);
 276  
 
 277  
 // remove the eventGroup as no further message will be received
 278  
 // for this group once we aggregate
 279  2
                     this.removeEventGroup(group);
 280  
 
 281  
 // add the new response message so that it can be collected by
 282  
 // the response Thread
 283  2
                     MuleMessage previousResult = (MuleMessage) responseMessages.putIfAbsent(groupId,
 284  
                             returnMessage);
 285  2
                     if (previousResult != null)
 286  
                     {
 287  
                         // this would indicate that we need a better way to prevent
 288  
                         // continued aggregation for a group that is currently being
 289  
                         // processed. Can this actually happen?
 290  0
                         throw new IllegalStateException(
 291  
                                 "Detected duplicate aggregation result message with id: " + groupId);
 292  
                     }
 293  
 
 294  
                     // will get/create a latch for the response Message ID and
 295  
                     // release it, notifying other threads that the response message
 296  
                     // is available
 297  2
                     Latch l = (Latch) locks.get(groupId);
 298  2
                     if (l == null)
 299  
                     {
 300  2
                         if (logger.isDebugEnabled())
 301  
                         {
 302  0
                             logger.debug("Creating latch for " + groupId + " in " + this);
 303  
                         }
 304  
 
 305  2
                         l = new Latch();
 306  2
                         Latch previous = (Latch) locks.putIfAbsent(groupId, l);
 307  2
                         if (previous != null)
 308  
                         {
 309  0
                             l = previous;
 310  
                         }
 311  
                     }
 312  
 
 313  2
                     l.countDown();
 314  
                 }
 315  
 
 316  
                 // result or not: exit spinloop
 317  6
                 break;
 318  0
             }
 319  
         }
 320  6
     }
 321  
 
 322  
     /**
 323  
      * @see org.mule.routing.inbound.AbstractEventAggregator#getEventGroup(Object)
 324  
      */
 325  
     protected EventGroup getEventGroup(Object groupId)
 326  
     {
 327  12
         return (EventGroup) eventGroups.get(groupId);
 328  
     }
 329  
 
 330  
     /**
 331  
      * @see org.mule.routing.inbound.AbstractEventAggregator#addEventGroup(EventGroup)
 332  
      */
 333  
     protected EventGroup addEventGroup(EventGroup group)
 334  
     {
 335  2
         EventGroup previous = (EventGroup) eventGroups.putIfAbsent(group.getGroupId(), group);
 336  
 // a parallel thread might have removed the EventGroup already,
 337  
 // therefore we need to validate our current reference
 338  2
         return (previous != null ? previous : group);
 339  
     }
 340  
 
 341  
     /**
 342  
      * @see org.mule.routing.inbound.AbstractEventAggregator#removeEventGroup(EventGroup)
 343  
      */
 344  
     protected void removeEventGroup(EventGroup group)
 345  
     {
 346  2
         eventGroups.remove(group.getGroupId());
 347  2
         addProcessedGroup(group.getGroupId());
 348  2
     }
 349  
 
 350  
     protected void addProcessedGroup(Object id)
 351  
     {
 352  2
         if (processedGroups.isFull())
 353  
         {
 354  0
             processedGroups.remove();
 355  
         }
 356  2
         processedGroups.add(id);
 357  2
     }
 358  
 
 359  
     protected boolean isGroupAlreadyProcessed(Object id)
 360  
     {
 361  6
         return processedGroups.contains(id);
 362  
     }
 363  
 
 364  
     /**
 365  
      * This method is called by the responding callee thread and should return the
 366  
      * aggregated response message
 367  
      *
 368  
      * @param message
 369  
      * @return
 370  
      * @throws RoutingException
 371  
      */
 372  
     public MuleMessage getResponse(MuleMessage message) throws RoutingException
 373  
     {
 374  0
         return getResponse(message, getTimeout());
 375  
     }
 376  
 
 377  
     /**
 378  
      * This method is called by the responding callee thread and should return the
 379  
      * aggregated response message
 380  
      *
 381  
      * @param message
 382  
      * @return
 383  
      * @throws RoutingException
 384  
      */
 385  
     public MuleMessage getResponse(MuleMessage message, int timeout) throws RoutingException
 386  
     {
 387  0
         Object responseId = messageInfoMapping.getMessageId(message);
 388  
 
 389  0
         if (logger.isDebugEnabled())
 390  
         {
 391  0
             logger.debug("Waiting for response for message id: " + responseId + " in " + this);
 392  
         }
 393  
 
 394  0
         Latch l = (Latch) locks.get(responseId);
 395  0
         if (l == null)
 396  
         {
 397  0
             if (logger.isDebugEnabled())
 398  
             {
 399  0
                 logger.debug("Got response but no one is waiting for it yet. Creating latch for "
 400  
                         + responseId + " in " + this);
 401  
             }
 402  
 
 403  0
             l = new Latch();
 404  0
             Latch previous = (Latch) locks.putIfAbsent(responseId, l);
 405  0
             if (previous != null)
 406  
             {
 407  0
                 l = previous;
 408  
             }
 409  
         }
 410  
 
 411  0
         if (logger.isDebugEnabled())
 412  
         {
 413  0
             logger.debug("Got latch for message: " + responseId);
 414  
         }
 415  
 
 416  
         // the final result message
 417  
         MuleMessage result;
 418  
 
 419  
 // indicates whether the result message could be obtained in the required
 420  
 // timeout interval
 421  0
         boolean resultAvailable = false;
 422  
 
 423  
 // flag for catching the interrupted status of the Thread waiting for a
 424  
 // result
 425  0
         boolean interruptedWhileWaiting = false;
 426  
 
 427  
         try
 428  
         {
 429  0
             if (logger.isDebugEnabled())
 430  
             {
 431  0
                 logger.debug("Waiting for response to message: " + responseId);
 432  
             }
 433  
 
 434  
             // how long should we wait for the lock?
 435  0
             if (this.getTimeout() <= 0)
 436  
             {
 437  0
                 l.await();
 438  0
                 resultAvailable = true;
 439  
             }
 440  
             else
 441  
             {
 442  0
                 resultAvailable = l.await(timeout, TimeUnit.MILLISECONDS);
 443  
             }
 444  
         }
 445  0
         catch (InterruptedException e)
 446  
         {
 447  0
             interruptedWhileWaiting = true;
 448  
         }
 449  
         finally
 450  
         {
 451  0
             locks.remove(responseId);
 452  0
             result = (MuleMessage) responseMessages.remove(responseId);
 453  
 
 454  0
             if (interruptedWhileWaiting)
 455  
             {
 456  0
                 Thread.currentThread().interrupt();
 457  
             }
 458  
         }
 459  
 
 460  0
         if (!resultAvailable)
 461  
         {
 462  0
             if (isFailOnTimeout())
 463  
             {
 464  0
                 if (logger.isTraceEnabled())
 465  
                 {
 466  0
                     logger.trace("Current responses are: \n" + MapUtils.toString(responseMessages, true));
 467  
                 }
 468  0
                 context.fireNotification(new RoutingNotification(message, null,
 469  
                         RoutingNotification.ASYNC_REPLY_TIMEOUT));
 470  
 
 471  0
                 throw new ResponseTimeoutException(
 472  
                         CoreMessages.responseTimedOutWaitingForId(
 473  
                                 this.getTimeout(), responseId), message, null);
 474  
             }
 475  
             else
 476  
             {
 477  0
                 EventGroup group = this.getEventGroup(responseId);
 478  0
                 if (group == null)
 479  
                 {
 480  
                     //Unlikely this will ever happen
 481  0
                     if (logger.isTraceEnabled())
 482  
                     {
 483  0
                         logger.trace("There is no current event Group. Current responses are: \n" + MapUtils.toString(responseMessages, true));
 484  
                     }
 485  0
                     return null;
 486  
                 }
 487  
                 else
 488  
                 {
 489  0
                     this.removeEventGroup(group);
 490  
 // create the response message
 491  0
                     MuleMessage msg = callback.aggregateEvents(group);
 492  0
                     return msg;
 493  
                 }
 494  
             }
 495  
         }
 496  
 
 497  0
         if (result == null)
 498  
         {
 499  
             // this should never happen, just using it as a safe guard for now
 500  0
             throw new IllegalStateException("Response Message is null");
 501  
         }
 502  
 
 503  0
         if (logger.isDebugEnabled())
 504  
         {
 505  0
             logger.debug("remaining locks  : " + locks.keySet());
 506  0
             logger.debug("remaining results: " + responseMessages.keySet());
 507  
         }
 508  
 
 509  0
         return result;
 510  
     }
 511  
 
 512  
 
 513  
     public boolean isFailOnTimeout()
 514  
     {
 515  0
         return failOnTimeout;
 516  
     }
 517  
 
 518  
     public void setFailOnTimeout(boolean failOnTimeout)
 519  
     {
 520  0
         this.failOnTimeout = failOnTimeout;
 521  0
     }
 522  
 
 523  
     public int getTimeout()
 524  
     {
 525  0
         return timeout;
 526  
     }
 527  
 
 528  
     public void setTimeout(int timeout)
 529  
     {
 530  0
         this.timeout = timeout;
 531  0
     }
 532  
 }