View Javadoc

1   /*
2    * $Id: UntilSuccessful.java 23213 2011-10-18 17:33:21Z 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;
12  
13  import java.io.Serializable;
14  import java.util.concurrent.TimeUnit;
15  
16  import org.mule.DefaultMuleEvent;
17  import org.mule.DefaultMuleMessage;
18  import org.mule.api.MessagingException;
19  import org.mule.api.MuleEvent;
20  import org.mule.api.MuleException;
21  import org.mule.api.MuleMessage;
22  import org.mule.api.MuleRuntimeException;
23  import org.mule.api.endpoint.EndpointBuilder;
24  import org.mule.api.endpoint.EndpointException;
25  import org.mule.api.lifecycle.InitialisationException;
26  import org.mule.api.processor.MessageProcessor;
27  import org.mule.api.retry.RetryCallback;
28  import org.mule.api.retry.RetryContext;
29  import org.mule.api.retry.RetryNotifier;
30  import org.mule.api.retry.RetryPolicyTemplate;
31  import org.mule.api.store.ListableObjectStore;
32  import org.mule.api.store.ObjectStoreException;
33  import org.mule.config.i18n.MessageFactory;
34  import org.mule.retry.async.AsynchronousRetryTemplate;
35  import org.mule.retry.policies.SimpleRetryPolicyTemplate;
36  import org.mule.routing.filters.ExpressionFilter;
37  import org.mule.routing.outbound.AbstractOutboundRouter;
38  import org.mule.util.SystemUtils;
39  
40  /**
41   * UntilSuccessful attempts to route a message to the message processor it contains in an asynchronous manner. Routing
42   * is considered successful if no exception has been raised and, optionally, if the response matches an expression.
43   * UntilSuccessful can optionally be configured to synchronously return an acknowledgment message when it has scheduled
44   * the event for processing. UntilSuccessful is backed by a {@link ListableObjectStore} for storing the events that are
45   * pending (re)processing.
46   */
47  public class UntilSuccessful extends AbstractOutboundRouter
48  {
49      public static class EventStoreKey implements Serializable
50      {
51          private static final long serialVersionUID = 1L;
52          private final String value;
53  
54          private EventStoreKey(final String value)
55          {
56              this.value = value;
57          }
58  
59          public static EventStoreKey buildFor(final MuleEvent muleEvent)
60          {
61              // the key is built in way to prevent UntilSuccessful workers across a cluster to compete for the same
62              // events over a shared object store
63              String key = muleEvent.getFlowConstruct() + "@"
64                  + muleEvent.getMuleContext().getClusterId() + ":" + muleEvent.getId();
65              return new EventStoreKey(SystemUtils.legalizeFileName(key));
66          }
67  
68          @Override
69          public String toString()
70          {
71              return value;
72          }
73  
74          @Override
75          public int hashCode()
76          {
77              return value.hashCode();
78          }
79  
80          @Override
81          public boolean equals(final Object obj)
82          {
83              if (!(obj instanceof EventStoreKey))
84              {
85                  return false;
86              }
87  
88              return value.equals(((EventStoreKey) obj).value);
89          }
90      }
91  
92      public static final String PROCESS_ATTEMPT_COUNT_PROPERTY_NAME = "process.attempt.count";
93  
94      private static final int DEFAULT_PROCESS_ATTEMPT_COUNT_PROPERTY_VALUE = 1;
95  
96      private ListableObjectStore<MuleEvent> objectStore;
97      private int maxRetries = 5;
98      private long secondsBetweenRetries = 60L;
99      private String failureExpression;
100     private String ackExpression;
101     private ExpressionFilter failureExpressionFilter;
102     private String eventKeyPrefix;
103     private Object deadLetterQueue;
104     private MessageProcessor dlqMP;
105 
106     @Override
107     public void initialise() throws InitialisationException
108     {
109         if (routes.isEmpty())
110         {
111             throw new InitialisationException(
112                 MessageFactory.createStaticMessage("One message processor must be configured within UntilSuccessful."),
113                 this);
114         }
115 
116         if (routes.size() > 1)
117         {
118             throw new InitialisationException(
119                 MessageFactory.createStaticMessage("Only one message processor is allowed within UntilSuccessful."
120                                                    + " Use a Processor Chain to group several message processors into one."),
121                 this);
122         }
123 
124         if (objectStore == null)
125         {
126             throw new InitialisationException(
127                 MessageFactory.createStaticMessage("A ListableObjectStore must be configured on UntilSuccessful."),
128                 this);
129         }
130 
131         super.initialise();
132 
133         if (deadLetterQueue != null)
134         {
135             if (deadLetterQueue instanceof EndpointBuilder)
136             {
137                 try
138                 {
139 
140                     dlqMP = ((EndpointBuilder) deadLetterQueue).buildOutboundEndpoint();
141                 }
142                 catch (final EndpointException ee)
143                 {
144                     throw new InitialisationException(
145                         MessageFactory.createStaticMessage("deadLetterQueue-ref is not a valid endpoint builder: "
146                                                            + deadLetterQueue), ee, this);
147                 }
148             }
149             else if (deadLetterQueue instanceof MessageProcessor)
150             {
151                 dlqMP = (MessageProcessor) deadLetterQueue;
152             }
153             else
154             {
155                 throw new InitialisationException(
156                     MessageFactory.createStaticMessage("deadLetterQueue-ref is not a valid mesage processor: "
157                                                        + deadLetterQueue), null, this);
158             }
159         }
160 
161         if (failureExpression != null)
162         {
163             failureExpressionFilter = new ExpressionFilter(failureExpression);
164         }
165         else
166         {
167             failureExpressionFilter = new ExpressionFilter("exception-type:");
168         }
169         failureExpressionFilter.setMuleContext(muleContext);
170 
171         if ((ackExpression != null) && (!muleContext.getExpressionManager().isExpression(ackExpression)))
172         {
173             throw new InitialisationException(MessageFactory.createStaticMessage("Invalid ackExpression: "
174                                                                                  + ackExpression), this);
175         }
176 
177         eventKeyPrefix = flowConstruct.getName() + "@" + muleContext.getClusterId() + ":";
178     }
179 
180     @Override
181     public void start() throws MuleException
182     {
183         super.start();
184         scheduleAllPendingEventsForProcessing();
185     }
186 
187     @Override
188     public boolean isMatch(final MuleMessage message) throws MuleException
189     {
190         return true;
191     }
192 
193     @Override
194     protected MuleEvent route(final MuleEvent event) throws MessagingException
195     {
196         try
197         {
198             ensurePayloadSerializable(event);
199         }
200         catch (final Exception e)
201         {
202             throw new MessagingException(
203                 MessageFactory.createStaticMessage("Failed to prepare message for processing"), event, e);
204         }
205 
206         try
207         {
208             final EventStoreKey eventStoreKey = storeEvent(event);
209             scheduleForProcessing(eventStoreKey);
210 
211             if (ackExpression == null)
212             {
213                 return null;
214             }
215 
216             final Object ackResponsePayload = muleContext.getExpressionManager().evaluate(ackExpression,
217                 event.getMessage());
218 
219             return new DefaultMuleEvent(new DefaultMuleMessage(ackResponsePayload, event.getMessage(),
220                 muleContext), event);
221         }
222         catch (final Exception e)
223         {
224             throw new MessagingException(
225                 MessageFactory.createStaticMessage("Failed to schedule the event for processing"), event, e);
226         }
227     }
228 
229     private void scheduleAllPendingEventsForProcessing() throws ObjectStoreException
230     {
231         for (final Serializable eventStoreKey : objectStore.allKeys())
232         {
233             try
234             {
235                 scheduleForProcessing((EventStoreKey) eventStoreKey);
236             }
237             catch (final Exception e)
238             {
239                 logger.error(
240                     MessageFactory.createStaticMessage("Failed to schedule for processing event stored with key: "
241                                                        + eventStoreKey), e);
242             }
243         }
244     }
245 
246     private void scheduleForProcessing(final EventStoreKey eventStoreKey) throws Exception
247     {
248         final RetryCallback callback = new RetryCallback()
249         {
250             @Override
251             public String getWorkDescription()
252             {
253                 return "Until successful processing of event stored under key: " + eventStoreKey;
254             }
255 
256             @Override
257             public void doWork(final RetryContext context) throws Exception
258             {
259                 retrieveAndProcessEvent(eventStoreKey);
260             }
261         };
262 
263         final SimpleRetryPolicyTemplate simpleRetryPolicyTemplate = new SimpleRetryPolicyTemplate(
264             TimeUnit.SECONDS.toMillis(secondsBetweenRetries), maxRetries);
265 
266         final RetryPolicyTemplate retryPolicyTemplate = new AsynchronousRetryTemplate(
267             simpleRetryPolicyTemplate);
268         retryPolicyTemplate.setNotifier(new RetryNotifier()
269         {
270             @Override
271             public void onSuccess(final RetryContext context)
272             {
273                 removeFromStore(eventStoreKey);
274             }
275 
276             @Override
277             public void onFailure(final RetryContext context, final Throwable e)
278             {
279                 incrementProcessAttemptCountOrRemoveFromStore(eventStoreKey);
280             }
281         });
282 
283         retryPolicyTemplate.execute(callback, muleContext.getWorkManager());
284     }
285 
286     private EventStoreKey storeEvent(final MuleEvent event) throws ObjectStoreException
287     {
288         final MuleMessage message = event.getMessage();
289         final Integer deliveryAttemptCount = message.getInvocationProperty(
290             PROCESS_ATTEMPT_COUNT_PROPERTY_NAME, DEFAULT_PROCESS_ATTEMPT_COUNT_PROPERTY_VALUE);
291         return storeEvent(event, deliveryAttemptCount);
292     }
293 
294     private EventStoreKey storeEvent(final MuleEvent event, final int deliveryAttemptCount)
295         throws ObjectStoreException
296     {
297         final MuleMessage message = event.getMessage();
298         message.setInvocationProperty(PROCESS_ATTEMPT_COUNT_PROPERTY_NAME, deliveryAttemptCount);
299         final EventStoreKey eventStoreKey = EventStoreKey.buildFor(event);
300         objectStore.store(eventStoreKey, event);
301         return eventStoreKey;
302     }
303 
304     private void incrementProcessAttemptCountOrRemoveFromStore(final EventStoreKey eventStoreKey)
305     {
306         try
307         {
308             final MuleEvent event = objectStore.remove(eventStoreKey);
309             final MuleEvent mutableEvent = threadSafeCopy(event);
310 
311             final MuleMessage message = mutableEvent.getMessage();
312             final Integer deliveryAttemptCount = message.getInvocationProperty(
313                 PROCESS_ATTEMPT_COUNT_PROPERTY_NAME, DEFAULT_PROCESS_ATTEMPT_COUNT_PROPERTY_VALUE);
314 
315             if (deliveryAttemptCount <= getMaxRetries())
316             {
317                 // we store the incremented version unless the max attempt count has been reached
318                 message.setInvocationProperty(PROCESS_ATTEMPT_COUNT_PROPERTY_NAME, deliveryAttemptCount + 1);
319                 objectStore.store(eventStoreKey, mutableEvent);
320             }
321             else
322             {
323                 abandonRetries(event, mutableEvent);
324             }
325         }
326         catch (final ObjectStoreException ose)
327         {
328             logger.error("Failed to increment failure count for event stored with key: " + eventStoreKey);
329         }
330     }
331 
332     private void abandonRetries(final MuleEvent event, final MuleEvent mutableEvent)
333     {
334         if (dlqMP == null)
335         {
336             logger.info("Retry attempts exhausted and no DLQ defined, dropping message: " + event);
337             return;
338         }
339 
340         try
341         {
342             logger.info("Retry attempts exhausted, routing message to DLQ: " + dlqMP);
343             dlqMP.process(mutableEvent);
344         }
345         catch (final MuleException me)
346         {
347             logger.error("Failed to route message to DLQ: " + dlqMP + ", dropping message: " + event, me);
348         }
349     }
350 
351     private void removeFromStore(final EventStoreKey eventStoreKey)
352     {
353         try
354         {
355             objectStore.remove(eventStoreKey);
356         }
357         catch (final ObjectStoreException ose)
358         {
359             logger.warn("Failed to remove following event from store with key: " + eventStoreKey);
360         }
361     }
362 
363     private void retrieveAndProcessEvent(final EventStoreKey eventStoreKey) throws ObjectStoreException
364     {
365         final MuleEvent persistedEvent = objectStore.retrieve(eventStoreKey);
366         final MuleEvent mutableEvent = threadSafeCopy(persistedEvent);
367         processEvent(mutableEvent);
368     }
369 
370     private void processEvent(final MuleEvent event)
371     {
372         if (routes.isEmpty())
373         {
374             return;
375         }
376 
377         MuleEvent returnEvent;
378         try
379         {
380             returnEvent = routes.get(0).process(event);
381         }
382         catch (final MuleException me)
383         {
384             throw new MuleRuntimeException(me);
385         }
386 
387         if (returnEvent == null)
388         {
389             return;
390         }
391 
392         final MuleMessage msg = returnEvent.getMessage();
393         if (msg == null)
394         {
395             throw new MuleRuntimeException(
396                 MessageFactory.createStaticMessage("No message found in response to processing, which is therefore considered failed for event: "
397                                                    + event));
398         }
399 
400         final boolean errorDetected = failureExpressionFilter.accept(msg);
401         if (errorDetected)
402         {
403             throw new MuleRuntimeException(
404                 MessageFactory.createStaticMessage("Failure expression positive when processing event: "
405                                                    + event));
406         }
407     }
408 
409     private DefaultMuleEvent threadSafeCopy(final MuleEvent event)
410     {
411         final DefaultMuleMessage message = new DefaultMuleMessage(event.getMessage().getPayload(),
412             event.getMessage(), muleContext);
413 
414         return new DefaultMuleEvent(message, event);
415     }
416 
417     private void ensurePayloadSerializable(final MuleEvent event) throws Exception
418     {
419         final MuleMessage message = event.getMessage();
420         if (message instanceof DefaultMuleMessage)
421         {
422             if (((DefaultMuleMessage) message).isConsumable())
423             {
424                 message.getPayloadAsBytes();
425             }
426         }
427         else
428         {
429             message.getPayloadAsBytes();
430         }
431     }
432 
433     public ListableObjectStore<MuleEvent> getObjectStore()
434     {
435         return objectStore;
436     }
437 
438     public void setObjectStore(final ListableObjectStore<MuleEvent> objectStore)
439     {
440         this.objectStore = objectStore;
441     }
442 
443     public int getMaxRetries()
444     {
445         return maxRetries;
446     }
447 
448     public void setMaxRetries(final int maxRetries)
449     {
450         this.maxRetries = maxRetries;
451     }
452 
453     public long getSecondsBetweenRetries()
454     {
455         return secondsBetweenRetries;
456     }
457 
458     public void setSecondsBetweenRetries(final long secondsBetweenRetries)
459     {
460         this.secondsBetweenRetries = secondsBetweenRetries;
461     }
462 
463     public String getFailureExpression()
464     {
465         return failureExpression;
466     }
467 
468     public void setFailureExpression(final String failureExpression)
469     {
470         this.failureExpression = failureExpression;
471     }
472 
473     public String getAckExpression()
474     {
475         return ackExpression;
476     }
477 
478     public void setAckExpression(final String ackExpression)
479     {
480         this.ackExpression = ackExpression;
481     }
482 
483     public void setDeadLetterQueue(final Object deadLetterQueue)
484     {
485         this.deadLetterQueue = deadLetterQueue;
486     }
487 
488     public Object getDeadLetterQueue()
489     {
490         return deadLetterQueue;
491     }
492 
493     public String getEventKeyPrefix()
494     {
495         return eventKeyPrefix;
496     }
497 
498     public ExpressionFilter getFailureExpressionFilter()
499     {
500         return failureExpressionFilter;
501     }
502 }