Coverage Report - org.mule.routing.correlation.EventCorrelator
 
Classes in this File Line Coverage Branch Coverage Complexity
EventCorrelator
0%
0/113
0%
0/48
0
EventCorrelator$ExpiringGroupWork
0%
0/30
0%
0/12
0
 
 1  
 /*
 2  
  * $Id: EventCorrelator.java 20781 2010-12-16 13:19:09Z dfeist $
 3  
  * --------------------------------------------------------------------------------------
 4  
  * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.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.correlation;
 11  
 
 12  
 import org.mule.DefaultMuleEvent;
 13  
 import org.mule.api.MuleContext;
 14  
 import org.mule.api.MuleEvent;
 15  
 import org.mule.api.MuleMessageCollection;
 16  
 import org.mule.api.construct.FlowConstruct;
 17  
 import org.mule.api.endpoint.ImmutableEndpoint;
 18  
 import org.mule.api.endpoint.InboundEndpoint;
 19  
 import org.mule.api.endpoint.OutboundEndpoint;
 20  
 import org.mule.api.processor.MessageProcessor;
 21  
 import org.mule.api.routing.MessageInfoMapping;
 22  
 import org.mule.api.routing.RoutingException;
 23  
 import org.mule.api.service.Service;
 24  
 import org.mule.config.i18n.CoreMessages;
 25  
 import org.mule.context.notification.RoutingNotification;
 26  
 import org.mule.routing.EventGroup;
 27  
 import org.mule.util.StringMessageUtils;
 28  
 import org.mule.util.concurrent.ThreadNameHelper;
 29  
 import org.mule.util.monitor.Expirable;
 30  
 import org.mule.util.monitor.ExpiryMonitor;
 31  
 
 32  
 import java.text.MessageFormat;
 33  
 import java.util.ArrayList;
 34  
 import java.util.List;
 35  
 import java.util.Map;
 36  
 
 37  
 import javax.resource.spi.work.Work;
 38  
 import javax.resource.spi.work.WorkException;
 39  
 
 40  
 import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
 41  
 import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap;
 42  
 import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
 43  
 import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean;
 44  
 import org.apache.commons.collections.buffer.BoundedFifoBuffer;
 45  
 import org.apache.commons.logging.Log;
 46  
 import org.apache.commons.logging.LogFactory;
 47  
 
 48  
 /**
 49  
  */
 50  0
 public class EventCorrelator
 51  
 {
 52  
     /**
 53  
      * logger used by this class
 54  
      */
 55  0
     protected transient final Log logger = LogFactory.getLog(EventCorrelator.class);
 56  
 
 57  
     public static final String NO_CORRELATION_ID = "no-id";
 58  
     
 59  
     public static final int MAX_PROCESSED_GROUPS = 50000;
 60  
 
 61  
     protected static final long MILLI_TO_NANO_MULTIPLIER = 1000000L;
 62  
     
 63  
     private static final long ONE_DAY_IN_MILLI = 1000 * 60 * 60 * 24;
 64  
 
 65  0
     protected long groupTimeToLive = ONE_DAY_IN_MILLI;
 66  
 
 67  
     /**
 68  
      * A map of EventGroup objects. These represent one or more messages to be
 69  
      * agregated, keyed by message id. There will be one response message for every
 70  
      * EventGroup.
 71  
      */
 72  0
     protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
 73  
 
 74  0
     protected final Object groupsLock = new Object();
 75  
 
 76  
     // @GuardedBy groupsLock
 77  0
     protected final BoundedFifoBuffer processedGroups = new BoundedFifoBuffer(MAX_PROCESSED_GROUPS);
 78  
 
 79  0
     private long timeout = -1; // undefined
 80  
 
 81  0
     private boolean failOnTimeout = true;
 82  
 
 83  
     private MessageInfoMapping messageInfoMapping;
 84  
 
 85  
     private MuleContext muleContext;
 86  
 
 87  
     private EventCorrelatorCallback callback;
 88  
 
 89  0
     private AtomicBoolean timerStarted = new AtomicBoolean(false);
 90  
     
 91  
     private MessageProcessor timeoutMessageProcessor;
 92  
     
 93  
     /**
 94  
      * A map with keys = group id and values = group creation time
 95  
      */
 96  0
     private Map expiredAndDispatchedGroups = new ConcurrentHashMap();
 97  
 
 98  
     public EventCorrelator(EventCorrelatorCallback callback, MessageProcessor timeoutMessageProcessor, MessageInfoMapping messageInfoMapping, MuleContext muleContext)
 99  0
     {
 100  0
         if (callback == null)
 101  
         {
 102  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("EventCorrelatorCallback").getMessage());
 103  
         }
 104  0
         if (messageInfoMapping == null)
 105  
         {
 106  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("MessageInfoMapping").getMessage());
 107  
         }
 108  0
         if (muleContext == null)
 109  
         {
 110  0
             throw new IllegalArgumentException(CoreMessages.objectIsNull("MuleContext").getMessage());
 111  
         }
 112  0
         this.callback = callback;
 113  0
         this.messageInfoMapping = messageInfoMapping;
 114  0
         this.muleContext = muleContext;
 115  0
         this.timeoutMessageProcessor = timeoutMessageProcessor;
 116  0
     }
 117  
 
 118  
     public void enableTimeoutMonitor() throws WorkException
 119  
     {
 120  0
         if (timerStarted.get())
 121  
         {
 122  0
             return;
 123  
         }
 124  
 
 125  0
         this.muleContext.getWorkManager().scheduleWork(new ExpiringGroupWork());
 126  0
     }
 127  
 
 128  
     public void forceGroupExpiry(String groupId)
 129  
     {
 130  0
         if (eventGroups.get(groupId) != null)
 131  
         {
 132  0
             handleGroupExpiry((EventGroup) eventGroups.get(groupId));
 133  
         }
 134  
         else
 135  
         {
 136  0
             addProcessedGroup(groupId);
 137  
         }
 138  0
     }
 139  
     
 140  
     public MuleEvent process(MuleEvent event) throws RoutingException
 141  
     {
 142  
         // the correlationId of the event's message
 143  0
         final String groupId = messageInfoMapping.getCorrelationId(event.getMessage());
 144  
 
 145  0
         if (logger.isTraceEnabled())
 146  
         {
 147  
             try
 148  
             {
 149  0
                 logger.trace(String.format("Received async reply message for correlationID: %s%n%s%n%s",
 150  
                                            groupId,
 151  
                                            StringMessageUtils.truncate(StringMessageUtils.toString(event.getMessage().getPayload()), 200, false),
 152  
                                            StringMessageUtils.headersToString(event.getMessage())));
 153  
             }
 154  0
             catch (Exception e)
 155  
             {
 156  
                 // ignore
 157  0
             }
 158  
         }
 159  0
         if (groupId == null || groupId.equals("-1"))
 160  
         {
 161  0
             throw new RoutingException(CoreMessages.noCorrelationId(), event, timeoutMessageProcessor);
 162  
         }
 163  
 
 164  
         // indicates interleaved EventGroup removal (very rare)
 165  0
         boolean lookupMiss = false;
 166  
 
 167  
         // spinloop for the EventGroup lookup
 168  
         while (true)
 169  
         {
 170  0
             if (lookupMiss)
 171  
             {
 172  
                 try
 173  
                 {
 174  
                     // recommended over Thread.yield()
 175  0
                     Thread.sleep(1);
 176  
                 }
 177  0
                 catch (InterruptedException interrupted)
 178  
                 {
 179  0
                     Thread.currentThread().interrupt();
 180  0
                 }
 181  
             }
 182  
 
 183  0
             if (isGroupAlreadyProcessed(groupId))
 184  
             {
 185  0
                 if (logger.isDebugEnabled())
 186  
                 {
 187  0
                     logger.debug("An event was received for an event group that has already been processed, " +
 188  
                             "this is probably because the async-reply timed out. Correlation Id is: " + groupId +
 189  
                             ". Dropping event");
 190  
                 }
 191  
                 //Fire a notification to say we received this message
 192  0
                 muleContext.fireNotification(new RoutingNotification(event.getMessage(),
 193  
                         event.getEndpoint().getEndpointURI().toString(),
 194  
                         RoutingNotification.MISSED_AGGREGATION_GROUP_EVENT));
 195  0
                 return null;
 196  
             }
 197  
             
 198  
             // check for an existing group first
 199  0
             EventGroup group = this.getEventGroup(groupId);
 200  
 
 201  
             // does the group exist?
 202  0
             if (group == null)
 203  
             {
 204  
                 // ..apparently not, so create a new one & add it
 205  0
                 group = this.addEventGroup(callback.createEventGroup(event, groupId));
 206  
             }
 207  
 
 208  
             // ensure that only one thread at a time evaluates this EventGroup
 209  0
             synchronized (groupsLock)
 210  
             {
 211  
                 // make sure no other thread removed the group in the meantime
 212  0
                 if (group != this.getEventGroup(groupId))
 213  
                 {
 214  
                     // if that is the (rare) case, spin
 215  0
                     lookupMiss = true;
 216  0
                     continue;
 217  
                 }
 218  
 
 219  0
                 if (logger.isDebugEnabled())
 220  
                 {
 221  0
                     logger.debug("Adding event to aggregator group: " + groupId);
 222  
                 }
 223  
 
 224  
                 // add the incoming event to the group
 225  0
                 group.addEvent(event);
 226  
 
 227  
                 // check to see if the event group is ready to be aggregated
 228  0
                 if (callback.shouldAggregateEvents(group))
 229  
                 {
 230  
                     // create the response event
 231  0
                     MuleEvent returnEvent = callback.aggregateEvents(group);
 232  0
                     returnEvent.getMessage().setCorrelationId(groupId);
 233  
 
 234  
                     // remove the eventGroup as no further message will be received
 235  
                     // for this group once we aggregate
 236  0
                     this.removeEventGroup(group);
 237  
 
 238  0
                     return returnEvent;
 239  
                 }
 240  
                 else
 241  
                 {
 242  0
                     return null;
 243  
                 }
 244  0
             }
 245  
         }
 246  
     }
 247  
     
 248  
 
 249  
     protected EventGroup getEventGroup(String groupId)
 250  
     {
 251  0
         return (EventGroup) eventGroups.get(groupId);
 252  
     }
 253  
 
 254  
     protected EventGroup addEventGroup(EventGroup group)
 255  
     {
 256  0
         EventGroup previous = (EventGroup) eventGroups.putIfAbsent(group.getGroupId(), group);
 257  
         // a parallel thread might have removed the EventGroup already,
 258  
         // therefore we need to validate our current reference
 259  0
         return (previous != null ? previous : group);
 260  
     }
 261  
 
 262  
     protected void removeEventGroup(EventGroup group)
 263  
     {
 264  0
         final Object groupId = group.getGroupId();
 265  0
         eventGroups.remove(groupId);
 266  0
         addProcessedGroup(groupId);
 267  0
     }
 268  
 
 269  
     protected void addProcessedGroup(Object id)
 270  
     {
 271  0
         synchronized (groupsLock)
 272  
         {
 273  0
             if (processedGroups.isFull())
 274  
             {
 275  0
                 processedGroups.remove();
 276  
             }
 277  0
             processedGroups.add(id);
 278  0
         }
 279  0
     }
 280  
 
 281  
     protected boolean isGroupAlreadyProcessed(Object id)
 282  
     {
 283  0
         synchronized (groupsLock)
 284  
         {
 285  0
             return processedGroups.contains(id);
 286  0
         }
 287  
     }
 288  
 
 289  
     public boolean isFailOnTimeout()
 290  
     {
 291  0
         return failOnTimeout;
 292  
     }
 293  
 
 294  
     public void setFailOnTimeout(boolean failOnTimeout)
 295  
     {
 296  0
         this.failOnTimeout = failOnTimeout;
 297  0
     }
 298  
 
 299  
     public long getTimeout()
 300  
     {
 301  0
         return timeout;
 302  
     }
 303  
 
 304  
     public void setTimeout(long timeout)
 305  
     {
 306  0
         this.timeout = timeout;
 307  0
     }
 308  
     
 309  
     protected void handleGroupExpiry(EventGroup group)
 310  
     {
 311  0
         removeEventGroup(group);
 312  
 
 313  0
         final FlowConstruct service = group.toArray()[0].getFlowConstruct();
 314  
 
 315  0
         if (isFailOnTimeout())
 316  
         {
 317  0
             final MuleMessageCollection messageCollection = group.toMessageCollection();
 318  0
             muleContext.fireNotification(new RoutingNotification(messageCollection, null,
 319  
                                                              RoutingNotification.CORRELATION_TIMEOUT));
 320  0
             service.getExceptionListener().handleException(
 321  
                     new CorrelationTimeoutException(CoreMessages.correlationTimedOut(group.getGroupId()),
 322  
                                                     group.getMessageCollectionEvent()), group.getMessageCollectionEvent());
 323  0
         }
 324  
         else
 325  
         {
 326  0
             if (logger.isDebugEnabled())
 327  
             {
 328  0
                 logger.debug(MessageFormat.format(
 329  
                         "Aggregator expired, but ''failOnTimeOut'' is false. Forwarding {0} events out of {1} " +
 330  
                         "total for group ID: {2}", group.size(), group.expectedSize(), group.getGroupId()
 331  
                 ));
 332  
             }
 333  
 
 334  
             try
 335  
             {
 336  0
                 if (!(group.getCreated() + groupTimeToLive < System.currentTimeMillis()))
 337  
                 {
 338  0
                     MuleEvent newEvent = callback.aggregateEvents(group);
 339  0
                     newEvent.getMessage().setCorrelationId(group.getGroupId().toString());
 340  
 
 341  
 
 342  0
                     if (!expiredAndDispatchedGroups.containsKey(group.getGroupId())) 
 343  
                     {
 344  
                         // TODO which use cases would need a sync reply event returned?
 345  0
                         if (timeoutMessageProcessor != null)
 346  
                         {
 347  0
                             if (timeoutMessageProcessor instanceof OutboundEndpoint)
 348  
                             {
 349  0
                                 newEvent = new DefaultMuleEvent(newEvent.getMessage(),
 350  
                                     (ImmutableEndpoint) timeoutMessageProcessor, newEvent.getSession());
 351  
                             }
 352  0
                             timeoutMessageProcessor.process(newEvent);
 353  
                         }
 354  
                         else
 355  
                         {
 356  0
                             if (!(service instanceof Service))
 357  
                             {
 358  0
                                 throw new UnsupportedOperationException("EventAggregator is only supported with Service");
 359  
                             }
 360  
 
 361  0
                             ((Service) service).dispatchEvent(newEvent);
 362  
                         }
 363  0
                         expiredAndDispatchedGroups.put(group.getGroupId(),
 364  
                             group.getCreated());
 365  
                     }
 366  
                     else
 367  
                     {
 368  0
                         logger.warn(MessageFormat.format("Discarding group {0}", group.getGroupId()));
 369  
                     }
 370  
                 }
 371  
             }
 372  0
             catch (Exception e)
 373  
             {
 374  0
                 service.getExceptionListener().handleException(e, group.getMessageCollectionEvent());
 375  0
             }
 376  
         }
 377  0
     }
 378  
 
 379  
     
 380  
     private final class ExpiringGroupWork implements Work, Expirable
 381  
     {
 382  
         private ExpiryMonitor expiryMonitor;
 383  
         
 384  
         public ExpiringGroupWork()
 385  0
         {
 386  0
             final String name = String.format("%sevent.correlator", ThreadNameHelper.getPrefix(muleContext));
 387  0
             this.expiryMonitor = new ExpiryMonitor(name, 1000 * 60);
 388  
             //clean up every 30 minutes
 389  0
             this.expiryMonitor.addExpirable(1000 * 60 * 30, TimeUnit.MILLISECONDS, this);
 390  0
         }
 391  
 
 392  
         /**
 393  
          * Removes the elements in expiredAndDispatchedGroups when groupLife is reached
 394  
          */
 395  
         public void expired()
 396  
         {
 397  0
             for (Object o : expiredAndDispatchedGroups.keySet())
 398  
             {
 399  0
                 Long time = (Long) expiredAndDispatchedGroups.get(o);
 400  0
                 if (time + groupTimeToLive < System.currentTimeMillis())
 401  
                 {
 402  0
                     expiredAndDispatchedGroups.remove(o);
 403  0
                     logger.warn(MessageFormat.format("Discarding group {0}", o));
 404  
                 }
 405  0
             }
 406  0
         }
 407  
 
 408  
         public void release()
 409  
         {
 410  
             //no op
 411  0
         }
 412  
 
 413  
         public void run()
 414  
         {
 415  
             while (true)
 416  
             {
 417  0
                 List<EventGroup> expired = new ArrayList<EventGroup>(1);
 418  0
                 for (Object o : eventGroups.values())
 419  
                 {
 420  0
                     EventGroup group = (EventGroup) o;
 421  0
                     if ((group.getCreated() + getTimeout() * MILLI_TO_NANO_MULTIPLIER) < System.nanoTime())
 422  
                     {
 423  0
                         expired.add(group);
 424  
                     }
 425  0
                 }
 426  0
                 if (expired.size() > 0)
 427  
                 {
 428  0
                     for (Object anExpired : expired)
 429  
                     {
 430  0
                         EventGroup group = (EventGroup) anExpired;
 431  0
                         handleGroupExpiry(group);
 432  0
                     }
 433  
                 }
 434  
                 try
 435  
                 {
 436  0
                     Thread.sleep(100);
 437  
                 }
 438  0
                 catch (InterruptedException e)
 439  
                 {
 440  0
                     break;
 441  0
                 }
 442  0
             }
 443  0
         }
 444  
 
 445  
     }
 446  
 }