View Javadoc

1   /*
2    * $Id: IdempotentInMemoryMessageIdStore.java 8991 2007-10-08 13:41:54Z holger $
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.inbound;
12  
13  import org.mule.config.i18n.CoreMessages;
14  import org.mule.util.concurrent.DaemonThreadFactory;
15  
16  import java.util.Map;
17  
18  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentSkipListMap;
19  import edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor;
20  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
21  import edu.emory.mathcs.backport.java.util.concurrent.helpers.Utils;
22  
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  
26  /**
27   * <code>IdempotentInMemoryMessageIdStore</code> implements an optionally bounded
28   * in-memory store for message IDs with periodic expiry of old entries. The bounded size
29   * is a <i>soft</i> limit and only enforced periodically by the expiry process; this
30   * means that the store may temporarily exceed its maximum size between expiry runs, but
31   * will eventually shrink to its configured size.
32   */
33  public class IdempotentInMemoryMessageIdStore implements IdempotentMessageIdStore
34  {
35      protected final Log logger = LogFactory.getLog(this.getClass());
36      protected final ConcurrentSkipListMap store;
37      protected final ScheduledThreadPoolExecutor scheduler;
38      protected final int maxEntries;
39      protected final int entryTTL;
40      protected final int expirationInterval;
41  
42      /**
43       * Default constructor for IdempotentInMemoryMessageIdStore.
44       * 
45       * @param name a name for this store, can be used for logging and identification
46       *            purposes
47       * @param maxEntries the maximum number of entries that this store keeps around.
48       *            Specify <em>-1</em> if the store is supposed to be "unbounded".
49       * @param entryTTL the time-to-live for each message ID, specified in seconds, or
50       *            <em>-1</em> for entries that should never expire. <b>DO NOT</b>
51       *            combine this with an unbounded store!
52       * @param expirationInterval the interval for periodic bounded size enforcement and
53       *            entry expiration, specified in seconds. Arbitrary positive values
54       *            between 1 second and several hours or days are possible, but should be
55       *            chosen carefully according to the expected message rate to prevent
56       *            OutOfMemory conditions.
57       * @see IdempotentReceiver#createMessageIdStore()
58       * @throws {@link IllegalArgumentException} if non-positive values are specified for
59       *             <code>expirationInterval</code>
60       */
61      public IdempotentInMemoryMessageIdStore(String name, int maxEntries, int entryTTL, int expirationInterval)
62      {
63          super();
64          this.store = new ConcurrentSkipListMap();
65          this.maxEntries = (maxEntries >= 0 ? maxEntries : Integer.MAX_VALUE);
66          this.entryTTL = entryTTL;
67  
68          if (expirationInterval <= 0)
69          {
70              throw new IllegalArgumentException(CoreMessages.propertyHasInvalidValue("expirationInterval",
71                  new Integer(expirationInterval)).toString());
72          }
73  
74          this.expirationInterval = expirationInterval;
75  
76          this.scheduler = new ScheduledThreadPoolExecutor(1);
77          scheduler.setThreadFactory(new DaemonThreadFactory(name + "-IdempotentMessageIdStore"));
78          scheduler.scheduleWithFixedDelay(new Expirer(), this.expirationInterval, this.expirationInterval,
79              TimeUnit.SECONDS);
80      }
81  
82      public boolean containsId(Object id) throws IllegalArgumentException, Exception
83      {
84          if (id == null)
85          {
86              throw new IllegalArgumentException(CoreMessages.objectIsNull("id").toString());
87          }
88  
89          // this is a relaxed check so we don't need to synchronize on the store.
90          return store.values().contains(id);
91      }
92  
93      public boolean storeId(Object id) throws IllegalArgumentException, Exception
94      {
95          if (id == null)
96          {
97              throw new IllegalArgumentException(CoreMessages.objectIsNull("id").toString());
98          }
99  
100         // this block is unfortunately necessary to counter a possible race condition
101         // between multiple nonatomic calls to containsId/storeId, which are
102         // only necessary because of the nonatomic calls to isMatch/process by
103         // InboundRouterCollection.route().
104         synchronized (store)
105         {
106             if (store.values().contains(id))
107             {
108                 return false;
109             }
110 
111             boolean written = false;
112             while (!written)
113             {
114                 written = (store.putIfAbsent(new Long(Utils.nanoTime()), id) == null);
115             }
116 
117             return true;
118         }
119     }
120 
121     protected void expire()
122     {
123         // this is not guaranteed to be precise, but we don't mind
124         int currentSize = store.size();
125 
126         // first trim to maxSize if necessary
127         int excess = (currentSize - maxEntries);
128         if (excess > 0)
129         {
130             while (currentSize > maxEntries)
131             {
132                 store.pollFirstEntry();
133                 currentSize--;
134             }
135 
136             if (logger.isDebugEnabled())
137             {
138                 logger.debug("Expired " + excess + " excess entries");
139             }
140         }
141 
142         // expire further if entry TTLs are enabled
143         if (entryTTL > 0 && currentSize != 0)
144         {
145             final long now = Utils.nanoTime();
146             int expiredEntries = 0;
147             Map.Entry oldestEntry;
148 
149             purge : while ((oldestEntry = store.firstEntry()) != null)
150             {
151                 Long oldestKey = (Long) oldestEntry.getKey();
152                 long oldestKeyValue = oldestKey.longValue();
153 
154                 if (TimeUnit.NANOSECONDS.toSeconds(now - oldestKeyValue) >= entryTTL)
155                 {
156                     store.remove(oldestKey);
157                     expiredEntries++;
158                 }
159                 else
160                 {
161                     break purge;
162                 }
163             }
164 
165             if (logger.isDebugEnabled())
166             {
167                 logger.debug("Expired " + expiredEntries + " old entries");
168             }
169         }
170     }
171 
172     protected class Expirer implements Runnable
173     {
174         public void run()
175         {
176             try
177             {
178                 // timed expiry MUST NOT throw anything..
179                 IdempotentInMemoryMessageIdStore.this.expire();
180             }
181             catch (Exception ex)
182             {
183                 // ..but if it does, at least log the error
184                 logger.error(ex.getMessage(), ex);
185             }
186         }
187     }
188 
189 }