View Javadoc

1   /*
2    * $Id: EventCorrelator.java 22751 2011-08-26 00:42:38Z mike.schilling $
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  
11  package org.mule.routing.correlation;
12  
13  import org.mule.api.MessagingException;
14  import org.mule.api.MuleContext;
15  import org.mule.api.MuleEvent;
16  import org.mule.api.MuleException;
17  import org.mule.api.MuleMessageCollection;
18  import org.mule.api.config.MuleProperties;
19  import org.mule.api.construct.FlowConstruct;
20  import org.mule.api.lifecycle.Startable;
21  import org.mule.api.lifecycle.Stoppable;
22  import org.mule.api.processor.MessageProcessor;
23  import org.mule.api.routing.MessageInfoMapping;
24  import org.mule.api.routing.RoutingException;
25  import org.mule.api.service.Service;
26  import org.mule.api.store.ListableObjectStore;
27  import org.mule.api.store.ObjectAlreadyExistsException;
28  import org.mule.api.store.ObjectDoesNotExistException;
29  import org.mule.api.store.ObjectStore;
30  import org.mule.api.store.ObjectStoreException;
31  import org.mule.api.store.ObjectStoreManager;
32  import org.mule.config.i18n.CoreMessages;
33  import org.mule.context.notification.RoutingNotification;
34  import org.mule.routing.EventGroup;
35  import org.mule.routing.EventProcessingThread;
36  import org.mule.util.StringMessageUtils;
37  import org.mule.util.concurrent.ThreadNameHelper;
38  import org.mule.util.monitor.Expirable;
39  import org.mule.util.monitor.ExpiryMonitor;
40  
41  import java.io.Serializable;
42  import java.text.MessageFormat;
43  import java.util.ArrayList;
44  import java.util.List;
45  import java.util.concurrent.TimeUnit;
46  
47  import org.apache.commons.logging.Log;
48  import org.apache.commons.logging.LogFactory;
49  
50  /**
51   */
52  public class EventCorrelator implements Startable, Stoppable
53  {
54      /**
55       * logger used by this class
56       */
57      protected transient final Log logger = LogFactory.getLog(EventCorrelator.class);
58  
59      public static final String NO_CORRELATION_ID = "no-id";
60  
61      public static final int MAX_PROCESSED_GROUPS = 50000;
62  
63      protected static final long MILLI_TO_NANO_MULTIPLIER = 1000000L;
64  
65      private static final long ONE_DAY_IN_MILLI = 1000 * 60 * 60 * 24;
66  
67      protected long groupTimeToLive = ONE_DAY_IN_MILLI;
68  
69      /**
70       * A map of EventGroup objects. These represent one or more messages to be
71       * agregated, keyed by message id. There will be one response message for every
72       * EventGroup.
73       */
74      protected ListableObjectStore<EventGroup> eventGroups;
75  
76      protected final Object groupsLock = new Object();
77  
78      // @GuardedBy groupsLock
79      protected ObjectStore<Long> processedGroups = null;
80  
81      private long timeout = -1; // undefined
82  
83      private boolean failOnTimeout = true;
84  
85      private MessageInfoMapping messageInfoMapping;
86  
87      private MuleContext muleContext;
88  
89      private EventCorrelatorCallback callback;
90  
91      private MessageProcessor timeoutMessageProcessor;
92  
93      /**
94       * A map with keys = group id and values = group creation time
95       */
96      private ListableObjectStore<Long> expiredAndDispatchedGroups = null;
97  
98      private EventCorrelator.ExpiringGroupMonitoringThread expiringGroupMonitoringThread;
99      private final String name;
100 
101     private final boolean persistentStores;
102     private final String storePrefix;
103 
104     public EventCorrelator(EventCorrelatorCallback callback,
105                            MessageProcessor timeoutMessageProcessor,
106                            MessageInfoMapping messageInfoMapping,
107                            MuleContext muleContext,
108                            String flowConstructName,
109                            boolean persistentStores,
110                            String storePrefix)
111     {
112         if (callback == null)
113         {
114             throw new IllegalArgumentException(CoreMessages.objectIsNull("EventCorrelatorCallback")
115                 .getMessage());
116         }
117         if (messageInfoMapping == null)
118         {
119             throw new IllegalArgumentException(CoreMessages.objectIsNull("MessageInfoMapping").getMessage());
120         }
121         if (muleContext == null)
122         {
123             throw new IllegalArgumentException(CoreMessages.objectIsNull("MuleContext").getMessage());
124         }
125         this.callback = callback;
126         this.messageInfoMapping = messageInfoMapping;
127         this.muleContext = muleContext;
128         this.timeoutMessageProcessor = timeoutMessageProcessor;
129         this.persistentStores = persistentStores;
130         this.storePrefix = storePrefix;
131         name = String.format("%s%s.event.correlator", ThreadNameHelper.getPrefix(muleContext),
132             flowConstructName);
133         ObjectStoreManager objectStoreManager = muleContext.getRegistry().get(
134             MuleProperties.OBJECT_STORE_MANAGER);
135         expiredAndDispatchedGroups = (ListableObjectStore<Long>) objectStoreManager.getObjectStore(
136             storePrefix + ".expiredAndDispatchedGroups", persistentStores);
137         processedGroups = (ListableObjectStore<Long>) objectStoreManager.getObjectStore(storePrefix
138                                                                                         + ".processedGroups",
139             persistentStores, MAX_PROCESSED_GROUPS, -1, 1000);
140         eventGroups = (ListableObjectStore<EventGroup>) objectStoreManager.getObjectStore(storePrefix
141                                                                                           + ".eventGroups",
142             persistentStores);
143     }
144 
145     public void forceGroupExpiry(String groupId) throws MessagingException
146     {
147         try
148         {
149             if (eventGroups.retrieve(groupId) != null)
150             {
151                 handleGroupExpiry((EventGroup) eventGroups.retrieve(groupId));
152             }
153             else
154             {
155                 addProcessedGroup(groupId);
156             }
157         }
158         catch (ObjectStoreException e)
159         {
160             // TODO improve this
161             throw new MessagingException(null, e);
162         }
163     }
164 
165     public MuleEvent process(MuleEvent event) throws RoutingException
166     {
167         // the correlationId of the event's message
168         final String groupId = messageInfoMapping.getCorrelationId(event.getMessage());
169 
170         if (logger.isTraceEnabled())
171         {
172             try
173             {
174                 logger.trace(String.format("Received async reply message for correlationID: %s%n%s%n%s",
175                     groupId, StringMessageUtils.truncate(
176                         StringMessageUtils.toString(event.getMessage().getPayload()), 200, false),
177                     StringMessageUtils.headersToString(event.getMessage())));
178             }
179             catch (Exception e)
180             {
181                 // ignore
182             }
183         }
184         if (groupId == null || groupId.equals("-1"))
185         {
186             throw new RoutingException(CoreMessages.noCorrelationId(), event, timeoutMessageProcessor);
187         }
188 
189         // indicates interleaved EventGroup removal (very rare)
190         boolean lookupMiss = false;
191 
192         // spinloop for the EventGroup lookup
193         while (true)
194         {
195             if (lookupMiss)
196             {
197                 try
198                 {
199                     // recommended over Thread.yield()
200                     Thread.sleep(1);
201                 }
202                 catch (InterruptedException interrupted)
203                 {
204                     Thread.currentThread().interrupt();
205                 }
206             }
207 
208             try
209             {
210                 if (isGroupAlreadyProcessed(groupId))
211                 {
212                     if (logger.isDebugEnabled())
213                     {
214                         logger.debug("An event was received for an event group that has already been processed, "
215                                      + "this is probably because the async-reply timed out. Correlation Id is: "
216                                      + groupId + ". Dropping event");
217                     }
218                     // Fire a notification to say we received this message
219                     muleContext.fireNotification(new RoutingNotification(event.getMessage(),
220                         event.getMessageSourceURI().toString(),
221                         RoutingNotification.MISSED_AGGREGATION_GROUP_EVENT));
222                     return null;
223                 }
224             }
225             catch (ObjectStoreException e)
226             {
227                 throw new RoutingException(event, timeoutMessageProcessor, e);
228             }
229 
230             // check for an existing group first
231             EventGroup group;
232             try
233             {
234                 group = this.getEventGroup(groupId);
235             }
236             catch (ObjectStoreException e)
237             {
238                 throw new RoutingException(event, timeoutMessageProcessor, e);
239             }
240 
241             // does the group exist?
242             if (group == null)
243             {
244                 // ..apparently not, so create a new one & add it
245                 try
246                 {
247                     group = this.addEventGroup(callback.createEventGroup(event, groupId));
248                 }
249                 catch (ObjectStoreException e)
250                 {
251                     throw new RoutingException(event, timeoutMessageProcessor, e);
252                 }
253             }
254 
255             // ensure that only one thread at a time evaluates this EventGroup
256             synchronized (groupsLock)
257             {
258                 if (logger.isDebugEnabled())
259                 {
260                     logger.debug("Adding event to aggregator group: " + groupId);
261                 }
262 
263                 // add the incoming event to the group
264                 try
265                 {
266                     group.addEvent(event);
267                 }
268                 catch (ObjectStoreException e)
269                 {
270                     throw new RoutingException(event, timeoutMessageProcessor, e);
271                 }
272 
273                 // check to see if the event group is ready to be aggregated
274                 if (callback.shouldAggregateEvents(group))
275                 {
276                     // create the response event
277                     MuleEvent returnEvent = callback.aggregateEvents(group);
278                     returnEvent.getMessage().setCorrelationId(groupId);
279                     String rootId = group.getCommonRootId();
280                     if (rootId != null)
281                     {
282                         returnEvent.getMessage().setMessageRootId(rootId);
283                     }
284 
285                     // remove the eventGroup as no further message will be received
286                     // for this group once we aggregate
287                     try
288                     {
289                         this.removeEventGroup(group);
290                         group.clear();
291                     }
292                     catch (ObjectStoreException e)
293                     {
294                         throw new RoutingException(event, timeoutMessageProcessor, e);
295                     }
296 
297                     return returnEvent;
298                 }
299                 else
300                 {
301                     return null;
302                 }
303             }
304         }
305     }
306 
307     protected EventGroup getEventGroup(String groupId) throws ObjectStoreException
308     {
309         try
310         {
311             return (EventGroup) eventGroups.retrieve(groupId);
312         }
313         catch (ObjectDoesNotExistException e)
314         {
315             return null;
316         }
317     }
318 
319     protected EventGroup addEventGroup(EventGroup group) throws ObjectStoreException
320     {
321         try
322         {
323             eventGroups.store((Serializable) group.getGroupId(), group);
324             return group;
325         }
326         catch (ObjectAlreadyExistsException e)
327         {
328             return (EventGroup) eventGroups.retrieve((Serializable) group.getGroupId());
329         }
330     }
331 
332     protected void removeEventGroup(EventGroup group) throws ObjectStoreException
333     {
334         final Object groupId = group.getGroupId();
335         eventGroups.remove((Serializable) groupId);
336         addProcessedGroup(groupId);
337     }
338 
339     protected void addProcessedGroup(Object id) throws ObjectStoreException
340     {
341         synchronized (groupsLock)
342         {
343             processedGroups.store((Serializable) id, System.nanoTime());
344         }
345     }
346 
347     protected boolean isGroupAlreadyProcessed(Object id) throws ObjectStoreException
348     {
349         synchronized (groupsLock)
350         {
351             return processedGroups.contains((Serializable) id);
352         }
353     }
354 
355     public boolean isFailOnTimeout()
356     {
357         return failOnTimeout;
358     }
359 
360     public void setFailOnTimeout(boolean failOnTimeout)
361     {
362         this.failOnTimeout = failOnTimeout;
363     }
364 
365     public long getTimeout()
366     {
367         return timeout;
368     }
369 
370     public void setTimeout(long timeout)
371     {
372         this.timeout = timeout;
373     }
374 
375     protected void handleGroupExpiry(EventGroup group) throws MessagingException
376     {
377         try
378         {
379             removeEventGroup(group);
380         }
381         catch (ObjectStoreException e)
382         {
383             throw new MessagingException(group.getMessageCollectionEvent(), e);
384         }
385 
386         if (isFailOnTimeout())
387         {
388             MuleMessageCollection messageCollection;
389             try
390             {
391                 messageCollection = group.toMessageCollection();
392             }
393             catch (ObjectStoreException e)
394             {
395                 throw new MessagingException(group.getMessageCollectionEvent(), e);
396             }
397             muleContext.fireNotification(new RoutingNotification(messageCollection, null,
398                 RoutingNotification.CORRELATION_TIMEOUT));
399             MuleEvent groupCollectionEvent = group.getMessageCollectionEvent();
400             try
401             {
402                 group.clear();
403             }
404             catch (ObjectStoreException e)
405             {
406                 logger.warn("Failed to clear group with id " + group.getGroupId()
407                             + " since underlying ObjectStore threw Exception:" + e.getMessage());
408             }
409             throw new CorrelationTimeoutException(CoreMessages.correlationTimedOut(group.getGroupId()),
410                 groupCollectionEvent);
411         }
412         else
413         {
414             if (logger.isDebugEnabled())
415             {
416                 logger.debug(MessageFormat.format(
417                     "Aggregator expired, but ''failOnTimeOut'' is false. Forwarding {0} events out of {1} "
418                                     + "total for group ID: {2}", group.size(), group.expectedSize(),
419                     group.getGroupId()));
420             }
421 
422             try
423             {
424                 if (!(group.getCreated() + groupTimeToLive < System.currentTimeMillis()))
425                 {
426                     MuleEvent newEvent = callback.aggregateEvents(group);
427                     group.clear();
428                     newEvent.getMessage().setCorrelationId(group.getGroupId().toString());
429 
430                     if (!expiredAndDispatchedGroups.contains((Serializable) group.getGroupId()))
431                     {
432                         // TODO which use cases would need a sync reply event
433                         // returned?
434                         if (timeoutMessageProcessor != null)
435                         {
436                             timeoutMessageProcessor.process(newEvent);
437                         }
438                         else
439                         {
440                             final FlowConstruct service = group.toArray()[0].getFlowConstruct();
441                             if (!(service instanceof Service))
442                             {
443                                 throw new UnsupportedOperationException(
444                                     "EventAggregator is only supported with Service");
445                             }
446 
447                             ((Service) service).dispatchEvent(newEvent);
448                         }
449                         expiredAndDispatchedGroups.store((Serializable) group.getGroupId(),
450                             group.getCreated());
451                     }
452                     else
453                     {
454                         logger.warn(MessageFormat.format("Discarding group {0}", group.getGroupId()));
455                     }
456                 }
457             }
458             catch (MessagingException me)
459             {
460                 throw me;
461             }
462             catch (Exception e)
463             {
464                 throw new MessagingException(group.getMessageCollectionEvent(), e);
465             }
466         }
467     }
468 
469     public void start() throws MuleException
470     {
471         logger.info("Starting event correlator: " + name);
472         if (timeout != 0)
473         {
474             expiringGroupMonitoringThread = new ExpiringGroupMonitoringThread();
475             expiringGroupMonitoringThread.start();
476         }
477     }
478 
479     public void stop() throws MuleException
480     {
481         logger.info("Stopping event correlator: " + name);
482         if (expiringGroupMonitoringThread != null)
483         {
484             expiringGroupMonitoringThread.stopProcessing();
485         }
486     }
487 
488     private final class ExpiringGroupMonitoringThread extends EventProcessingThread implements Expirable
489     {
490         private ExpiryMonitor expiryMonitor;
491         public static final long DELAY_TIME =  10;
492 
493         public ExpiringGroupMonitoringThread()
494         {
495             super(name, DELAY_TIME);
496             this.expiryMonitor = new ExpiryMonitor(name, 1000 * 60, muleContext, true);
497             // clean up every 30 minutes
498             this.expiryMonitor.addExpirable(1000 * 60 * 30, TimeUnit.MILLISECONDS, this);
499         }
500 
501         /**
502          * Removes the elements in expiredAndDispatchedGroups when groupLife is
503          * reached
504          * 
505          * @throws ObjectStoreException
506          */
507         public void expired()
508         {
509             try
510             {
511                 for (Serializable o : expiredAndDispatchedGroups.allKeys())
512                 {
513                     Long time = (Long) expiredAndDispatchedGroups.retrieve(o);
514                     if (time + groupTimeToLive < System.currentTimeMillis())
515                     {
516                         expiredAndDispatchedGroups.remove(o);
517                         logger.warn(MessageFormat.format("Discarding group {0}", o));
518                     }
519                 }
520             }
521             catch (ObjectStoreException e)
522             {
523                 logger.warn("Expiration of objects failed due to ObjectStoreException " + e + ".");
524             }
525         }
526 
527         public void doRun()
528         {
529             List<EventGroup> expired = new ArrayList<EventGroup>(1);
530             try
531             {
532                 for (Serializable o : eventGroups.allKeys())
533                 {
534                     EventGroup group = (EventGroup) eventGroups.retrieve(o);
535                     if ((group.getCreated() + getTimeout() * MILLI_TO_NANO_MULTIPLIER) < System.nanoTime())
536                     {
537                         expired.add(group);
538                     }
539                 }
540             }
541             catch (ObjectStoreException e)
542             {
543                 logger.warn("expiry failed dues to ObjectStoreException " + e);
544             }
545             if (expired.size() > 0)
546             {
547                 for (Object anExpired : expired)
548                 {
549                     EventGroup group = (EventGroup) anExpired;
550                     try
551                     {
552                         handleGroupExpiry(group);
553                     }
554                     catch (MessagingException e)
555                     {
556                         e.getEvent()
557                             .getFlowConstruct()
558                             .getExceptionListener()
559                             .handleException(e, e.getEvent());
560                     }
561                 }
562             }
563         }
564     }
565 }