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}