View Javadoc

1   /*
2    * $Id: AbstractResponseAggregator.java 7976 2007-08-21 14:26:13Z 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  
11  package org.mule.routing.response;
12  
13  import org.mule.config.i18n.CoreMessages;
14  import org.mule.routing.inbound.AbstractEventAggregator;
15  import org.mule.routing.inbound.EventGroup;
16  import org.mule.umo.UMOEvent;
17  import org.mule.umo.UMOMessage;
18  import org.mule.umo.routing.ResponseTimeoutException;
19  import org.mule.umo.routing.RoutingException;
20  import org.mule.util.MapUtils;
21  import org.mule.util.concurrent.Latch;
22  
23  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
24  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap;
25  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
26  
27  /**
28   * <code>AbstractResponseAggregator</code> provides a base class for implementing
29   * response aggregator routers. This provides a thread-safe implemenetation and
30   * allows developers to customise how and when events are grouped and collated.
31   * Response Agrregators are used to collect responses that are usually sent to
32   * replyTo endpoints set on outbound routers. When an event is sent out via an
33   * outbound router, the response router will block the response flow on an
34   * UMOComponent until the Response Router resolves a reply or times out.
35   */
36  public abstract class AbstractResponseAggregator extends AbstractResponseRouter
37  {
38      /**
39       * A map of EventGroup objects. These represent one or more messages to be
40       * agregated, keyed by message id. There will be one response message for every
41       * EventGroup.
42       */
43      protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
44  
45      /**
46       * A map of locks used to wait for response messages for a given message id
47       */
48      protected final ConcurrentMap locks = new ConcurrentHashMap();
49  
50      /**
51       * The collection of messages that are ready to be returned to the callee. Keyed
52       * by Message ID
53       */
54      protected final ConcurrentMap responseMessages = new ConcurrentHashMap();
55  
56      public void process(UMOEvent event) throws RoutingException
57      {
58          // the correlationId of the event's message
59          final Object groupId = this.getReplyAggregateIdentifier(event.getMessage());
60          if (groupId == null || groupId.equals("-1"))
61          {
62              throw new RoutingException(CoreMessages.noCorrelationId(), event.getMessage(), event
63                  .getEndpoint());
64          }
65  
66          // indicates interleaved EventGroup removal (very rare)
67          boolean lookupMiss = false;
68  
69          // spinloop for the EventGroup lookup
70          while (true)
71          {
72              if (lookupMiss)
73              {
74                  try
75                  {
76                      // recommended over Thread.yield()
77                      Thread.sleep(1);
78                  }
79                  catch (InterruptedException interrupted)
80                  {
81                      Thread.currentThread().interrupt();
82                  }
83              }
84  
85              // check for an existing group first
86              EventGroup group = this.getEventGroup(groupId);
87  
88              // does the group exist?
89              if (group == null)
90              {
91                  // ..apparently not, so create a new one & add it
92                  group = this.addEventGroup(this.createEventGroup(event, groupId));
93              }
94  
95              // ensure that only one thread at a time evaluates this EventGroup
96              synchronized (group)
97              {
98                  // make sure no other thread removed the group in the meantime
99                  if (group != this.getEventGroup(groupId))
100                 {
101                     // if that is the (rare) case, spin
102                     lookupMiss = true;
103                     continue;
104                 }
105 
106                 if (logger.isDebugEnabled())
107                 {
108                     logger.debug("Adding event to response aggregator group: " + groupId);
109                 }
110 
111                 // add the incoming event to the group
112                 group.addEvent(event);
113 
114                 // check to see if the event group is ready to be aggregated
115                 if (this.shouldAggregateEvents(group))
116                 {
117                     // create the response message
118                     UMOMessage returnMessage = this.aggregateEvents(group);
119 
120                     // remove the eventGroup as no further message will be received
121                     // for this group once we aggregate
122                     this.removeEventGroup(group);
123 
124                     // add the new response message so that it can be collected by
125                     // the response Thread
126                     UMOMessage previousResult = (UMOMessage) responseMessages.putIfAbsent(groupId,
127                         returnMessage);
128                     if (previousResult != null)
129                     {
130                         // this would indicate that we need a better way to prevent
131                         // continued aggregation for a group that is currently being
132                         // processed. Can this actually happen?
133                         throw new IllegalStateException(
134                             "Detected duplicate aggregation result message with id: " + groupId);
135                     }
136 
137                     // will get/create a latch for the response Message ID and
138                     // release it, notifying other threads that the response message
139                     // is available
140                     Latch l = (Latch) locks.get(groupId);
141                     if (l == null)
142                     {
143                         if (logger.isDebugEnabled())
144                         {
145                             logger.debug("Creating latch for " + groupId + " in " + this);
146                         }
147 
148                         l = new Latch();
149                         Latch previous = (Latch) locks.putIfAbsent(groupId, l);
150                         if (previous != null)
151                         {
152                             l = previous;
153                         }
154                     }
155 
156                     l.countDown();
157                 }
158 
159                 // result or not: exit spinloop
160                 break;
161             }
162         }
163     }
164 
165     /**
166      * @see AbstractEventAggregator#createEventGroup(UMOEvent, Object)
167      */
168     protected EventGroup createEventGroup(UMOEvent event, Object groupId)
169     {
170         if (logger.isDebugEnabled())
171         {
172             logger.debug("Creating new event group: " + groupId + " in " + this);
173         }
174 
175         return new EventGroup(groupId);
176     }
177 
178     /**
179      * @see AbstractEventAggregator#getEventGroup(Object)
180      */
181     protected EventGroup getEventGroup(Object groupId)
182     {
183         return (EventGroup) eventGroups.get(groupId);
184     }
185 
186     /**
187      * @see AbstractEventAggregator#addEventGroup(EventGroup)
188      */
189     protected EventGroup addEventGroup(EventGroup group)
190     {
191         EventGroup previous = (EventGroup) eventGroups.putIfAbsent(group.getGroupId(), group);
192         // a parallel thread might have removed the EventGroup already,
193         // therefore we need to validate our current reference
194         return (previous != null ? previous : group);
195     }
196 
197     /**
198      * @see AbstractEventAggregator#removeEventGroup(EventGroup)
199      */
200     protected void removeEventGroup(EventGroup group)
201     {
202         eventGroups.remove(group.getGroupId());
203     }
204 
205     /**
206      * This method is called by the responding callee thread and should return the
207      * aggregated response message
208      * 
209      * @param message
210      * @return
211      * @throws RoutingException
212      */
213     public UMOMessage getResponse(UMOMessage message) throws RoutingException
214     {
215         Object responseId = this.getCallResponseAggregateIdentifier(message);
216 
217         if (logger.isDebugEnabled())
218         {
219             logger.debug("Waiting for response for message id: " + responseId + " in " + this);
220         }
221 
222         Latch l = (Latch) locks.get(responseId);
223         if (l == null)
224         {
225             if (logger.isDebugEnabled())
226             {
227                 logger.debug("Got response but no one is waiting for it yet. Creating latch for "
228                                 + responseId + " in " + this);
229             }
230 
231             l = new Latch();
232             Latch previous = (Latch) locks.putIfAbsent(responseId, l);
233             if (previous != null)
234             {
235                 l = previous;
236             }
237         }
238 
239         if (logger.isDebugEnabled())
240         {
241             logger.debug("Got latch for message: " + responseId);
242         }
243 
244         // the final result message
245         UMOMessage result;
246 
247         // indicates whether the result message could be obtained in the required
248         // timeout interval
249         boolean resultAvailable = false;
250 
251         // flag for catching the interrupted status of the Thread waiting for a
252         // result
253         boolean interruptedWhileWaiting = false;
254 
255         try
256         {
257             if (logger.isDebugEnabled())
258             {
259                 logger.debug("Waiting for response to message: " + responseId);
260             }
261 
262             // how long should we wait for the lock?
263             if (this.getTimeout() <= 0)
264             {
265                 l.await();
266                 resultAvailable = true;
267             }
268             else
269             {
270                 resultAvailable = l.await(this.getTimeout(), TimeUnit.MILLISECONDS);
271             }
272         }
273         catch (InterruptedException e)
274         {
275             interruptedWhileWaiting = true;
276         }
277         finally
278         {
279             locks.remove(responseId);
280             result = (UMOMessage) responseMessages.remove(responseId);
281 
282             if (interruptedWhileWaiting)
283             {
284                 Thread.currentThread().interrupt();
285             }
286         }
287 
288         if (!resultAvailable)
289         {
290             if (logger.isTraceEnabled())
291             {
292                 logger.trace("Current responses are: \n" + MapUtils.toString(responseMessages, true));
293             }
294 
295             throw new ResponseTimeoutException(
296                 CoreMessages.responseTimedOutWaitingForId(
297                     this.getTimeout(), responseId), message, null);
298         }
299 
300         if (result == null)
301         {
302             // this should never happen, just using it as a safe guard for now
303             throw new IllegalStateException("Response Message is null");
304         }
305 
306         if (logger.isDebugEnabled())
307         {
308             logger.debug("remaining locks  : " + locks.keySet());
309             logger.debug("remaining results: " + responseMessages.keySet());
310         }
311 
312         return result;
313     }
314 
315     /**
316      * @see AbstractEventAggregator#shouldAggregateEvents(EventGroup)
317      */
318     protected abstract boolean shouldAggregateEvents(EventGroup events);
319 
320     /**
321      * @see AbstractEventAggregator#aggregateEvents(EventGroup)
322      */
323     protected abstract UMOMessage aggregateEvents(EventGroup events) throws RoutingException;
324 
325 }