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