View Javadoc

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  public class EventCorrelator
51  {
52      /**
53       * logger used by this class
54       */
55      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      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      protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
73  
74      protected final Object groupsLock = new Object();
75  
76      // @GuardedBy groupsLock
77      protected final BoundedFifoBuffer processedGroups = new BoundedFifoBuffer(MAX_PROCESSED_GROUPS);
78  
79      private long timeout = -1; // undefined
80  
81      private boolean failOnTimeout = true;
82  
83      private MessageInfoMapping messageInfoMapping;
84  
85      private MuleContext muleContext;
86  
87      private EventCorrelatorCallback callback;
88  
89      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      private Map expiredAndDispatchedGroups = new ConcurrentHashMap();
97  
98      public EventCorrelator(EventCorrelatorCallback callback, MessageProcessor timeoutMessageProcessor, MessageInfoMapping messageInfoMapping, MuleContext muleContext)
99      {
100         if (callback == null)
101         {
102             throw new IllegalArgumentException(CoreMessages.objectIsNull("EventCorrelatorCallback").getMessage());
103         }
104         if (messageInfoMapping == null)
105         {
106             throw new IllegalArgumentException(CoreMessages.objectIsNull("MessageInfoMapping").getMessage());
107         }
108         if (muleContext == null)
109         {
110             throw new IllegalArgumentException(CoreMessages.objectIsNull("MuleContext").getMessage());
111         }
112         this.callback = callback;
113         this.messageInfoMapping = messageInfoMapping;
114         this.muleContext = muleContext;
115         this.timeoutMessageProcessor = timeoutMessageProcessor;
116     }
117 
118     public void enableTimeoutMonitor() throws WorkException
119     {
120         if (timerStarted.get())
121         {
122             return;
123         }
124 
125         this.muleContext.getWorkManager().scheduleWork(new ExpiringGroupWork());
126     }
127 
128     public void forceGroupExpiry(String groupId)
129     {
130         if (eventGroups.get(groupId) != null)
131         {
132             handleGroupExpiry((EventGroup) eventGroups.get(groupId));
133         }
134         else
135         {
136             addProcessedGroup(groupId);
137         }
138     }
139     
140     public MuleEvent process(MuleEvent event) throws RoutingException
141     {
142         // the correlationId of the event's message
143         final String groupId = messageInfoMapping.getCorrelationId(event.getMessage());
144 
145         if (logger.isTraceEnabled())
146         {
147             try
148             {
149                 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             catch (Exception e)
155             {
156                 // ignore
157             }
158         }
159         if (groupId == null || groupId.equals("-1"))
160         {
161             throw new RoutingException(CoreMessages.noCorrelationId(), event, timeoutMessageProcessor);
162         }
163 
164         // indicates interleaved EventGroup removal (very rare)
165         boolean lookupMiss = false;
166 
167         // spinloop for the EventGroup lookup
168         while (true)
169         {
170             if (lookupMiss)
171             {
172                 try
173                 {
174                     // recommended over Thread.yield()
175                     Thread.sleep(1);
176                 }
177                 catch (InterruptedException interrupted)
178                 {
179                     Thread.currentThread().interrupt();
180                 }
181             }
182 
183             if (isGroupAlreadyProcessed(groupId))
184             {
185                 if (logger.isDebugEnabled())
186                 {
187                     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                 muleContext.fireNotification(new RoutingNotification(event.getMessage(),
193                         event.getEndpoint().getEndpointURI().toString(),
194                         RoutingNotification.MISSED_AGGREGATION_GROUP_EVENT));
195                 return null;
196             }
197             
198             // check for an existing group first
199             EventGroup group = this.getEventGroup(groupId);
200 
201             // does the group exist?
202             if (group == null)
203             {
204                 // ..apparently not, so create a new one & add it
205                 group = this.addEventGroup(callback.createEventGroup(event, groupId));
206             }
207 
208             // ensure that only one thread at a time evaluates this EventGroup
209             synchronized (groupsLock)
210             {
211                 // make sure no other thread removed the group in the meantime
212                 if (group != this.getEventGroup(groupId))
213                 {
214                     // if that is the (rare) case, spin
215                     lookupMiss = true;
216                     continue;
217                 }
218 
219                 if (logger.isDebugEnabled())
220                 {
221                     logger.debug("Adding event to aggregator group: " + groupId);
222                 }
223 
224                 // add the incoming event to the group
225                 group.addEvent(event);
226 
227                 // check to see if the event group is ready to be aggregated
228                 if (callback.shouldAggregateEvents(group))
229                 {
230                     // create the response event
231                     MuleEvent returnEvent = callback.aggregateEvents(group);
232                     returnEvent.getMessage().setCorrelationId(groupId);
233 
234                     // remove the eventGroup as no further message will be received
235                     // for this group once we aggregate
236                     this.removeEventGroup(group);
237 
238                     return returnEvent;
239                 }
240                 else
241                 {
242                     return null;
243                 }
244             }
245         }
246     }
247     
248 
249     protected EventGroup getEventGroup(String groupId)
250     {
251         return (EventGroup) eventGroups.get(groupId);
252     }
253 
254     protected EventGroup addEventGroup(EventGroup group)
255     {
256         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         return (previous != null ? previous : group);
260     }
261 
262     protected void removeEventGroup(EventGroup group)
263     {
264         final Object groupId = group.getGroupId();
265         eventGroups.remove(groupId);
266         addProcessedGroup(groupId);
267     }
268 
269     protected void addProcessedGroup(Object id)
270     {
271         synchronized (groupsLock)
272         {
273             if (processedGroups.isFull())
274             {
275                 processedGroups.remove();
276             }
277             processedGroups.add(id);
278         }
279     }
280 
281     protected boolean isGroupAlreadyProcessed(Object id)
282     {
283         synchronized (groupsLock)
284         {
285             return processedGroups.contains(id);
286         }
287     }
288 
289     public boolean isFailOnTimeout()
290     {
291         return failOnTimeout;
292     }
293 
294     public void setFailOnTimeout(boolean failOnTimeout)
295     {
296         this.failOnTimeout = failOnTimeout;
297     }
298 
299     public long getTimeout()
300     {
301         return timeout;
302     }
303 
304     public void setTimeout(long timeout)
305     {
306         this.timeout = timeout;
307     }
308     
309     protected void handleGroupExpiry(EventGroup group)
310     {
311         removeEventGroup(group);
312 
313         final FlowConstruct service = group.toArray()[0].getFlowConstruct();
314 
315         if (isFailOnTimeout())
316         {
317             final MuleMessageCollection messageCollection = group.toMessageCollection();
318             muleContext.fireNotification(new RoutingNotification(messageCollection, null,
319                                                              RoutingNotification.CORRELATION_TIMEOUT));
320             service.getExceptionListener().handleException(
321                     new CorrelationTimeoutException(CoreMessages.correlationTimedOut(group.getGroupId()),
322                                                     group.getMessageCollectionEvent()), group.getMessageCollectionEvent());
323         }
324         else
325         {
326             if (logger.isDebugEnabled())
327             {
328                 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                 if (!(group.getCreated() + groupTimeToLive < System.currentTimeMillis()))
337                 {
338                     MuleEvent newEvent = callback.aggregateEvents(group);
339                     newEvent.getMessage().setCorrelationId(group.getGroupId().toString());
340 
341 
342                     if (!expiredAndDispatchedGroups.containsKey(group.getGroupId())) 
343                     {
344                         // TODO which use cases would need a sync reply event returned?
345                         if (timeoutMessageProcessor != null)
346                         {
347                             if (timeoutMessageProcessor instanceof OutboundEndpoint)
348                             {
349                                 newEvent = new DefaultMuleEvent(newEvent.getMessage(),
350                                     (ImmutableEndpoint) timeoutMessageProcessor, newEvent.getSession());
351                             }
352                             timeoutMessageProcessor.process(newEvent);
353                         }
354                         else
355                         {
356                             if (!(service instanceof Service))
357                             {
358                                 throw new UnsupportedOperationException("EventAggregator is only supported with Service");
359                             }
360 
361                             ((Service) service).dispatchEvent(newEvent);
362                         }
363                         expiredAndDispatchedGroups.put(group.getGroupId(),
364                             group.getCreated());
365                     }
366                     else
367                     {
368                         logger.warn(MessageFormat.format("Discarding group {0}", group.getGroupId()));
369                     }
370                 }
371             }
372             catch (Exception e)
373             {
374                 service.getExceptionListener().handleException(e, group.getMessageCollectionEvent());
375             }
376         }
377     }
378 
379     
380     private final class ExpiringGroupWork implements Work, Expirable
381     {
382         private ExpiryMonitor expiryMonitor;
383         
384         public ExpiringGroupWork()
385         {
386             final String name = String.format("%sevent.correlator", ThreadNameHelper.getPrefix(muleContext));
387             this.expiryMonitor = new ExpiryMonitor(name, 1000 * 60);
388             //clean up every 30 minutes
389             this.expiryMonitor.addExpirable(1000 * 60 * 30, TimeUnit.MILLISECONDS, this);
390         }
391 
392         /**
393          * Removes the elements in expiredAndDispatchedGroups when groupLife is reached
394          */
395         public void expired()
396         {
397             for (Object o : expiredAndDispatchedGroups.keySet())
398             {
399                 Long time = (Long) expiredAndDispatchedGroups.get(o);
400                 if (time + groupTimeToLive < System.currentTimeMillis())
401                 {
402                     expiredAndDispatchedGroups.remove(o);
403                     logger.warn(MessageFormat.format("Discarding group {0}", o));
404                 }
405             }
406         }
407 
408         public void release()
409         {
410             //no op
411         }
412 
413         public void run()
414         {
415             while (true)
416             {
417                 List<EventGroup> expired = new ArrayList<EventGroup>(1);
418                 for (Object o : eventGroups.values())
419                 {
420                     EventGroup group = (EventGroup) o;
421                     if ((group.getCreated() + getTimeout() * MILLI_TO_NANO_MULTIPLIER) < System.nanoTime())
422                     {
423                         expired.add(group);
424                     }
425                 }
426                 if (expired.size() > 0)
427                 {
428                     for (Object anExpired : expired)
429                     {
430                         EventGroup group = (EventGroup) anExpired;
431                         handleGroupExpiry(group);
432                     }
433                 }
434                 try
435                 {
436                     Thread.sleep(100);
437                 }
438                 catch (InterruptedException e)
439                 {
440                     break;
441                 }
442             }
443         }
444 
445     }
446 }