View Javadoc

1   /*
2    * $Id: EventCorrelator.java 11567 2008-04-11 13:08:05Z dirk.olmes $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.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;
11  
12  import org.mule.api.MuleContext;
13  import org.mule.api.MuleEvent;
14  import org.mule.api.MuleMessage;
15  import org.mule.api.routing.MessageInfoMapping;
16  import org.mule.api.routing.ResponseTimeoutException;
17  import org.mule.api.routing.RoutingException;
18  import org.mule.config.i18n.CoreMessages;
19  import org.mule.context.notification.RoutingNotification;
20  import org.mule.routing.inbound.EventGroup;
21  import org.mule.util.MapUtils;
22  import org.mule.util.concurrent.Latch;
23  
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  
30  import javax.resource.spi.work.Work;
31  import javax.resource.spi.work.WorkException;
32  
33  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
34  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap;
35  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
36  import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean;
37  
38  import org.apache.commons.collections.buffer.BoundedFifoBuffer;
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  
42  /**
43   * TODO
44   */
45  public class EventCorrelator
46  {
47      /**
48       * logger used by this class
49       */
50      protected transient final Log logger = LogFactory.getLog(EventCorrelator.class);
51  
52      public static final String NO_CORRELATION_ID = "no-id";
53      
54      public static final int MAX_PROCESSED_GROUPS = 50000;
55      /**
56       * A map of EventGroup objects. These represent one or more messages to be
57       * agregated, keyed by message id. There will be one response message for every
58       * EventGroup.
59       */
60      protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
61  
62      /**
63       * A map of locks used to wait for response messages for a given message id
64       */
65      protected final ConcurrentMap locks = new ConcurrentHashMap();
66  
67      /**
68       * The collection of messages that are ready to be returned to the callee. Keyed
69       * by Message ID
70       */
71      protected final ConcurrentMap responseMessages = new ConcurrentHashMap();
72  
73      protected final BoundedFifoBuffer processedGroups = new BoundedFifoBuffer(MAX_PROCESSED_GROUPS);
74  
75      private int 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  
88      public EventCorrelator(EventCorrelatorCallback callback, MessageInfoMapping messageInfoMapping, MuleContext context)
89      {
90          if (callback == null)
91          {
92              throw new IllegalArgumentException(CoreMessages.objectIsNull("EventCorrelatorCallback").getMessage());
93          }
94          if (messageInfoMapping == null)
95          {
96              throw new IllegalArgumentException(CoreMessages.objectIsNull("MessageInfoMapping").getMessage());
97          }
98          if (context == null)
99          {
100             throw new IllegalArgumentException(CoreMessages.objectIsNull("MuleContext").getMessage());
101         }
102         this.callback = callback;
103         this.messageInfoMapping = messageInfoMapping;
104         this.context = context;
105 
106 
107     }
108 
109     public void enableTimeoutMonitor() throws WorkException
110     {
111         if (!timerStarted.get())
112         {
113 
114             this.context.getWorkManager().scheduleWork(new Work()
115             {
116                 public void release()
117                 {
118                     //no op
119                 }
120 
121 
122                 public void run()
123                 {
124                     while (true)
125                     {
126                         List expired = new ArrayList(1);
127                         for (Iterator iterator = eventGroups.values().iterator(); iterator.hasNext();)
128                         {
129                             EventGroup group = (EventGroup) iterator.next();
130                             if ((group.getCreated() + getTimeout()) < System.currentTimeMillis())
131                             {
132                                 expired.add(group);
133                             }
134                         }
135                         if (expired.size() > 0)
136                         {
137                             for (Iterator iterator = expired.iterator(); iterator.hasNext();)
138                             {
139                                 EventGroup group = (EventGroup) iterator.next();
140                                 eventGroups.remove(group.getGroupId());
141                                 locks.remove(group.getGroupId());
142 
143                                 context.fireNotification(new RoutingNotification(group.toMessageCollection(), null,
144                                         RoutingNotification.CORRELATION_TIMEOUT));
145 
146 //                            if(isFailOnTimeout())
147 //                            {
148                                 group.toArray()[0].getService().getExceptionListener().exceptionThrown(
149                                         new CorrelationTimeoutException(CoreMessages.correlationTimedOut(group.getGroupId()),
150                                                 group.toMessageCollection()));
151 //                            }
152 //                            else
153 //                            {
154 //                                //We could invoke a callback on the compoennt here or just dispatch the events??
155 //                            }
156                             }
157                         }
158                         try
159                         {
160                             Thread.sleep(100);
161                         }
162                         catch (InterruptedException e)
163                         {
164                             break;
165                         }
166                     }
167                 }
168             }
169 
170             );
171         }
172     }
173 
174     /**
175      * @return
176      * @deprecated this is used by a test, but I would like to remove this method
177      */
178     public Map getResponseMessages()
179     {
180         return Collections.unmodifiableMap(responseMessages);
181     }
182 
183     public MuleMessage process(MuleEvent event) throws RoutingException
184     {
185         addEvent(event);
186         Object correlationId = messageInfoMapping.getCorrelationId(event.getMessage());
187         if (locks.get(correlationId) != null)
188         {
189             locks.remove(correlationId);
190             return (MuleMessage) responseMessages.remove(correlationId);
191         }
192         else
193         {
194             return null;
195         }
196     }
197 
198     public void addEvent(MuleEvent event) throws RoutingException
199     {
200 // the correlationId of the event's message
201         final Object groupId = messageInfoMapping.getCorrelationId(event.getMessage());
202 
203         if (groupId == null || groupId.equals("-1"))
204         {
205             throw new RoutingException(CoreMessages.noCorrelationId(), event.getMessage(), event
206                     .getEndpoint());
207         }
208 
209         // indicates interleaved EventGroup removal (very rare)
210         boolean lookupMiss = false;
211 
212 // spinloop for the EventGroup lookup
213         while (true)
214         {
215             if (lookupMiss)
216             {
217                 try
218                 {
219                     // recommended over Thread.yield()
220                     Thread.sleep(1);
221                 }
222                 catch (InterruptedException interrupted)
223                 {
224                     Thread.currentThread().interrupt();
225                 }
226             }
227 
228             if (isGroupAlreadyProcessed(groupId))
229             {
230                 if (logger.isDebugEnabled())
231                 {
232                     logger.debug("An event was received for an event group that has already been processed, " +
233                             "this is probably because the async-reply timed out. Correlation Id is: " + groupId +
234                             ". Dropping event");
235                 }
236                 //Fire a notification to say we received this message
237                 context.fireNotification(new RoutingNotification(event.getMessage(),
238                         event.getEndpoint().getEndpointURI().toString(),
239                         RoutingNotification.MISSED_ASYNC_REPLY));
240                 return;
241             }
242             // check for an existing group first
243             EventGroup group = this.getEventGroup(groupId);
244 
245 // does the group exist?
246             if (group == null)
247             {
248                 // ..apparently not, so create a new one & add it
249                 group = this.addEventGroup(callback.createEventGroup(event, groupId));
250             }
251 
252 // ensure that only one thread at a time evaluates this EventGroup
253             synchronized (group)
254             {
255                 // make sure no other thread removed the group in the meantime
256                 if (group != this.getEventGroup(groupId))
257                 {
258                     // if that is the (rare) case, spin
259                     lookupMiss = true;
260                     continue;
261                 }
262 
263                 if (logger.isDebugEnabled())
264                 {
265                     logger.debug("Adding event to response aggregator group: " + groupId);
266                 }
267 
268                 // add the incoming event to the group
269                 group.addEvent(event);
270 
271 // check to see if the event group is ready to be aggregated
272                 if (callback.shouldAggregateEvents(group))
273                 {
274                     // create the response message
275                     MuleMessage returnMessage = callback.aggregateEvents(group);
276 
277 // remove the eventGroup as no further message will be received
278 // for this group once we aggregate
279                     this.removeEventGroup(group);
280 
281 // add the new response message so that it can be collected by
282 // the response Thread
283                     MuleMessage previousResult = (MuleMessage) responseMessages.putIfAbsent(groupId,
284                             returnMessage);
285                     if (previousResult != null)
286                     {
287                         // this would indicate that we need a better way to prevent
288                         // continued aggregation for a group that is currently being
289                         // processed. Can this actually happen?
290                         throw new IllegalStateException(
291                                 "Detected duplicate aggregation result message with id: " + groupId);
292                     }
293 
294                     // will get/create a latch for the response Message ID and
295                     // release it, notifying other threads that the response message
296                     // is available
297                     Latch l = (Latch) locks.get(groupId);
298                     if (l == null)
299                     {
300                         if (logger.isDebugEnabled())
301                         {
302                             logger.debug("Creating latch for " + groupId + " in " + this);
303                         }
304 
305                         l = new Latch();
306                         Latch previous = (Latch) locks.putIfAbsent(groupId, l);
307                         if (previous != null)
308                         {
309                             l = previous;
310                         }
311                     }
312 
313                     l.countDown();
314                 }
315 
316                 // result or not: exit spinloop
317                 break;
318             }
319         }
320     }
321 
322     /**
323      * @see org.mule.routing.inbound.AbstractEventAggregator#getEventGroup(Object)
324      */
325     protected EventGroup getEventGroup(Object groupId)
326     {
327         return (EventGroup) eventGroups.get(groupId);
328     }
329 
330     /**
331      * @see org.mule.routing.inbound.AbstractEventAggregator#addEventGroup(EventGroup)
332      */
333     protected EventGroup addEventGroup(EventGroup group)
334     {
335         EventGroup previous = (EventGroup) eventGroups.putIfAbsent(group.getGroupId(), group);
336 // a parallel thread might have removed the EventGroup already,
337 // therefore we need to validate our current reference
338         return (previous != null ? previous : group);
339     }
340 
341     /**
342      * @see org.mule.routing.inbound.AbstractEventAggregator#removeEventGroup(EventGroup)
343      */
344     protected void removeEventGroup(EventGroup group)
345     {
346         eventGroups.remove(group.getGroupId());
347         addProcessedGroup(group.getGroupId());
348     }
349 
350     protected void addProcessedGroup(Object id)
351     {
352         if (processedGroups.isFull())
353         {
354             processedGroups.remove();
355         }
356         processedGroups.add(id);
357     }
358 
359     protected boolean isGroupAlreadyProcessed(Object id)
360     {
361         return processedGroups.contains(id);
362     }
363 
364     /**
365      * This method is called by the responding callee thread and should return the
366      * aggregated response message
367      *
368      * @param message
369      * @return
370      * @throws RoutingException
371      */
372     public MuleMessage getResponse(MuleMessage message) throws RoutingException
373     {
374         return getResponse(message, getTimeout());
375     }
376 
377     /**
378      * This method is called by the responding callee thread and should return the
379      * aggregated response message
380      *
381      * @param message
382      * @return
383      * @throws RoutingException
384      */
385     public MuleMessage getResponse(MuleMessage message, int timeout) throws RoutingException
386     {
387         Object responseId = messageInfoMapping.getMessageId(message);
388 
389         if (logger.isDebugEnabled())
390         {
391             logger.debug("Waiting for response for message id: " + responseId + " in " + this);
392         }
393 
394         Latch l = (Latch) locks.get(responseId);
395         if (l == null)
396         {
397             if (logger.isDebugEnabled())
398             {
399                 logger.debug("Got response but no one is waiting for it yet. Creating latch for "
400                         + responseId + " in " + this);
401             }
402 
403             l = new Latch();
404             Latch previous = (Latch) locks.putIfAbsent(responseId, l);
405             if (previous != null)
406             {
407                 l = previous;
408             }
409         }
410 
411         if (logger.isDebugEnabled())
412         {
413             logger.debug("Got latch for message: " + responseId);
414         }
415 
416         // the final result message
417         MuleMessage result;
418 
419 // indicates whether the result message could be obtained in the required
420 // timeout interval
421         boolean resultAvailable = false;
422 
423 // flag for catching the interrupted status of the Thread waiting for a
424 // result
425         boolean interruptedWhileWaiting = false;
426 
427         try
428         {
429             if (logger.isDebugEnabled())
430             {
431                 logger.debug("Waiting for response to message: " + responseId);
432             }
433 
434             // how long should we wait for the lock?
435             if (this.getTimeout() <= 0)
436             {
437                 l.await();
438                 resultAvailable = true;
439             }
440             else
441             {
442                 resultAvailable = l.await(timeout, TimeUnit.MILLISECONDS);
443             }
444         }
445         catch (InterruptedException e)
446         {
447             interruptedWhileWaiting = true;
448         }
449         finally
450         {
451             locks.remove(responseId);
452             result = (MuleMessage) responseMessages.remove(responseId);
453 
454             if (interruptedWhileWaiting)
455             {
456                 Thread.currentThread().interrupt();
457             }
458         }
459 
460         if (!resultAvailable)
461         {
462             if (isFailOnTimeout())
463             {
464                 if (logger.isTraceEnabled())
465                 {
466                     logger.trace("Current responses are: \n" + MapUtils.toString(responseMessages, true));
467                 }
468                 context.fireNotification(new RoutingNotification(message, null,
469                         RoutingNotification.ASYNC_REPLY_TIMEOUT));
470 
471                 throw new ResponseTimeoutException(
472                         CoreMessages.responseTimedOutWaitingForId(
473                                 this.getTimeout(), responseId), message, null);
474             }
475             else
476             {
477                 EventGroup group = this.getEventGroup(responseId);
478                 if (group == null)
479                 {
480                     //Unlikely this will ever happen
481                     if (logger.isTraceEnabled())
482                     {
483                         logger.trace("There is no current event Group. Current responses are: \n" + MapUtils.toString(responseMessages, true));
484                     }
485                     return null;
486                 }
487                 else
488                 {
489                     this.removeEventGroup(group);
490 // create the response message
491                     MuleMessage msg = callback.aggregateEvents(group);
492                     return msg;
493                 }
494             }
495         }
496 
497         if (result == null)
498         {
499             // this should never happen, just using it as a safe guard for now
500             throw new IllegalStateException("Response Message is null");
501         }
502 
503         if (logger.isDebugEnabled())
504         {
505             logger.debug("remaining locks  : " + locks.keySet());
506             logger.debug("remaining results: " + responseMessages.keySet());
507         }
508 
509         return result;
510     }
511 
512 
513     public boolean isFailOnTimeout()
514     {
515         return failOnTimeout;
516     }
517 
518     public void setFailOnTimeout(boolean failOnTimeout)
519     {
520         this.failOnTimeout = failOnTimeout;
521     }
522 
523     public int getTimeout()
524     {
525         return timeout;
526     }
527 
528     public void setTimeout(int timeout)
529     {
530         this.timeout = timeout;
531     }
532 }