001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.support; 018 019import java.util.ArrayList; 020import java.util.Comparator; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.concurrent.ConcurrentMap; 026import java.util.concurrent.ScheduledExecutorService; 027import java.util.concurrent.ScheduledFuture; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.locks.Lock; 030import java.util.concurrent.locks.ReentrantLock; 031 032import org.apache.camel.TimeoutMap; 033import org.apache.camel.util.ObjectHelper; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037/** 038 * Default implementation of the {@link TimeoutMap}. 039 * <p/> 040 * This implementation supports thread safe and non thread safe, in the manner you can enable locking or not. 041 * By default locking is enabled and thus we are thread safe. 042 * <p/> 043 * You must provide a {@link java.util.concurrent.ScheduledExecutorService} in the constructor which is used 044 * to schedule a background task which check for old entries to purge. This implementation will shutdown the scheduler 045 * if its being stopped. 046 * You must also invoke {@link #start()} to startup the timeout map, before its ready to be used. 047 * And you must invoke {@link #stop()} to stop the map when no longer in use. 048 * 049 * @version 050 */ 051public class DefaultTimeoutMap<K, V> extends ServiceSupport implements TimeoutMap<K, V>, Runnable { 052 053 protected final Logger log = LoggerFactory.getLogger(getClass()); 054 055 private final ConcurrentMap<K, TimeoutMapEntry<K, V>> map = new ConcurrentHashMap<>(); 056 private final ScheduledExecutorService executor; 057 private volatile ScheduledFuture<?> future; 058 private final long purgePollTime; 059 private final Lock lock = new ReentrantLock(); 060 private boolean useLock = true; 061 062 public DefaultTimeoutMap(ScheduledExecutorService executor) { 063 this(executor, 1000); 064 } 065 066 public DefaultTimeoutMap(ScheduledExecutorService executor, long requestMapPollTimeMillis) { 067 this(executor, requestMapPollTimeMillis, true); 068 } 069 070 public DefaultTimeoutMap(ScheduledExecutorService executor, long requestMapPollTimeMillis, boolean useLock) { 071 ObjectHelper.notNull(executor, "ScheduledExecutorService"); 072 this.executor = executor; 073 this.purgePollTime = requestMapPollTimeMillis; 074 this.useLock = useLock; 075 } 076 077 public V get(K key) { 078 TimeoutMapEntry<K, V> entry; 079 if (useLock) { 080 lock.lock(); 081 } 082 try { 083 entry = map.get(key); 084 if (entry == null) { 085 return null; 086 } 087 updateExpireTime(entry); 088 } finally { 089 if (useLock) { 090 lock.unlock(); 091 } 092 } 093 return entry.getValue(); 094 } 095 096 public V put(K key, V value, long timeoutMillis) { 097 TimeoutMapEntry<K, V> entry = new TimeoutMapEntry<>(key, value, timeoutMillis); 098 if (useLock) { 099 lock.lock(); 100 } 101 try { 102 updateExpireTime(entry); 103 TimeoutMapEntry<K, V> result = map.put(key, entry); 104 return result != null ? result.getValue() : null; 105 } finally { 106 if (useLock) { 107 lock.unlock(); 108 } 109 } 110 } 111 112 public V putIfAbsent(K key, V value, long timeoutMillis) { 113 TimeoutMapEntry<K, V> entry = new TimeoutMapEntry<>(key, value, timeoutMillis); 114 if (useLock) { 115 lock.lock(); 116 } 117 try { 118 updateExpireTime(entry); 119 //Just make sure we don't override the old entry 120 TimeoutMapEntry<K, V> result = map.putIfAbsent(key, entry); 121 return result != null ? result.getValue() : null; 122 } finally { 123 if (useLock) { 124 lock.unlock(); 125 } 126 } 127 } 128 129 public V remove(K key) { 130 TimeoutMapEntry<K, V> entry; 131 132 if (useLock) { 133 lock.lock(); 134 } 135 try { 136 entry = map.remove(key); 137 } finally { 138 if (useLock) { 139 lock.unlock(); 140 } 141 } 142 143 return entry != null ? entry.getValue() : null; 144 } 145 146 public Object[] getKeys() { 147 Object[] keys; 148 if (useLock) { 149 lock.lock(); 150 } 151 try { 152 Set<K> keySet = map.keySet(); 153 keys = new Object[keySet.size()]; 154 keySet.toArray(keys); 155 } finally { 156 if (useLock) { 157 lock.unlock(); 158 } 159 } 160 return keys; 161 } 162 163 public int size() { 164 return map.size(); 165 } 166 167 /** 168 * The timer task which purges old requests and schedules another poll 169 */ 170 public void run() { 171 // only run if allowed 172 if (!isRunAllowed()) { 173 log.trace("Purge task not allowed to run"); 174 return; 175 } 176 177 log.trace("Running purge task to see if any entries has been timed out"); 178 try { 179 purge(); 180 } catch (Throwable t) { 181 // must catch and log exception otherwise the executor will now schedule next run 182 log.warn("Exception occurred during purge task. This exception will be ignored.", t); 183 } 184 } 185 186 public void purge() { 187 log.trace("There are {} in the timeout map", map.size()); 188 if (map.isEmpty()) { 189 return; 190 } 191 192 long now = currentTime(); 193 194 List<TimeoutMapEntry<K, V>> expired = new ArrayList<>(); 195 196 if (useLock) { 197 lock.lock(); 198 } 199 try { 200 // need to find the expired entries and add to the expired list 201 for (Map.Entry<K, TimeoutMapEntry<K, V>> entry : map.entrySet()) { 202 if (entry.getValue().getExpireTime() < now) { 203 if (isValidForEviction(entry.getValue())) { 204 log.debug("Evicting inactive entry ID: {}", entry.getValue()); 205 expired.add(entry.getValue()); 206 } 207 } 208 } 209 210 // if we found any expired then we need to sort, onEviction and remove 211 if (!expired.isEmpty()) { 212 // sort according to the expired time so we got the first expired first 213 expired.sort(new Comparator<TimeoutMapEntry<K, V>>() { 214 public int compare(TimeoutMapEntry<K, V> a, TimeoutMapEntry<K, V> b) { 215 long diff = a.getExpireTime() - b.getExpireTime(); 216 if (diff == 0) { 217 return 0; 218 } 219 return diff > 0 ? 1 : -1; 220 } 221 }); 222 223 List<K> evicts = new ArrayList<>(expired.size()); 224 try { 225 // now fire eviction notification 226 for (TimeoutMapEntry<K, V> entry : expired) { 227 boolean evict = false; 228 try { 229 evict = onEviction(entry.getKey(), entry.getValue()); 230 } catch (Throwable t) { 231 log.warn("Exception happened during eviction of entry ID {}, won't evict and will continue trying: {}", 232 entry.getValue(), t); 233 } 234 if (evict) { 235 // okay this entry should be evicted 236 evicts.add(entry.getKey()); 237 } 238 } 239 } finally { 240 // and must remove from list after we have fired the notifications 241 for (K key : evicts) { 242 map.remove(key); 243 } 244 } 245 } 246 } finally { 247 if (useLock) { 248 lock.unlock(); 249 } 250 } 251 } 252 253 // Properties 254 // ------------------------------------------------------------------------- 255 256 public long getPurgePollTime() { 257 return purgePollTime; 258 } 259 260 public ScheduledExecutorService getExecutor() { 261 return executor; 262 } 263 264 // Implementation methods 265 // ------------------------------------------------------------------------- 266 267 /** 268 * lets schedule each time to allow folks to change the time at runtime 269 */ 270 protected void schedulePoll() { 271 future = executor.scheduleWithFixedDelay(this, 0, purgePollTime, TimeUnit.MILLISECONDS); 272 } 273 274 /** 275 * A hook to allow derivations to avoid evicting the current entry 276 */ 277 protected boolean isValidForEviction(TimeoutMapEntry<K, V> entry) { 278 return true; 279 } 280 281 public boolean onEviction(K key, V value) { 282 return true; 283 } 284 285 protected void updateExpireTime(TimeoutMapEntry<K, V> entry) { 286 long now = currentTime(); 287 entry.setExpireTime(entry.getTimeout() + now); 288 } 289 290 protected long currentTime() { 291 return System.currentTimeMillis(); 292 } 293 294 @Override 295 protected void doStart() throws Exception { 296 if (executor.isShutdown()) { 297 throw new IllegalStateException("The ScheduledExecutorService is shutdown"); 298 } 299 schedulePoll(); 300 } 301 302 @Override 303 protected void doStop() throws Exception { 304 if (future != null) { 305 future.cancel(false); 306 future = null; 307 } 308 // clear map if we stop 309 map.clear(); 310 } 311 312}