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