View Javadoc

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