1
2
3
4
5
6
7
8
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
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
69
70
71
72 protected final ConcurrentMap eventGroups = new ConcurrentHashMap();
73
74 protected final Object groupsLock = new Object();
75
76
77 protected final BoundedFifoBuffer processedGroups = new BoundedFifoBuffer(MAX_PROCESSED_GROUPS);
78
79 private long timeout = -1;
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
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
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
157 }
158 }
159 if (groupId == null || groupId.equals("-1"))
160 {
161 throw new RoutingException(CoreMessages.noCorrelationId(), event, timeoutMessageProcessor);
162 }
163
164
165 boolean lookupMiss = false;
166
167
168 while (true)
169 {
170 if (lookupMiss)
171 {
172 try
173 {
174
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
192 muleContext.fireNotification(new RoutingNotification(event.getMessage(),
193 event.getEndpoint().getEndpointURI().toString(),
194 RoutingNotification.MISSED_AGGREGATION_GROUP_EVENT));
195 return null;
196 }
197
198
199 EventGroup group = this.getEventGroup(groupId);
200
201
202 if (group == null)
203 {
204
205 group = this.addEventGroup(callback.createEventGroup(event, groupId));
206 }
207
208
209 synchronized (groupsLock)
210 {
211
212 if (group != this.getEventGroup(groupId))
213 {
214
215 lookupMiss = true;
216 continue;
217 }
218
219 if (logger.isDebugEnabled())
220 {
221 logger.debug("Adding event to aggregator group: " + groupId);
222 }
223
224
225 group.addEvent(event);
226
227
228 if (callback.shouldAggregateEvents(group))
229 {
230
231 MuleEvent returnEvent = callback.aggregateEvents(group);
232 returnEvent.getMessage().setCorrelationId(groupId);
233
234
235
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
258
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
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
389 this.expiryMonitor.addExpirable(1000 * 60 * 30, TimeUnit.MILLISECONDS, this);
390 }
391
392
393
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
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 }