View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.http.impl.client.cache;
28  
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.ConcurrentMap;
32  
33  import org.apache.http.annotation.Contract;
34  import org.apache.http.annotation.ThreadingBehavior;
35  
36  /**
37   * Implements a bounded failure cache. The oldest entries are discarded when
38   * the maximum size is exceeded.
39   *
40   * @since 4.3
41   */
42  @Contract(threading = ThreadingBehavior.SAFE)
43  public class DefaultFailureCache implements FailureCache {
44  
45      static final int DEFAULT_MAX_SIZE = 1000;
46      static final int MAX_UPDATE_TRIES = 10;
47  
48      private final int maxSize;
49      private final ConcurrentMap<String, FailureCacheValue> storage;
50  
51      /**
52       * Create a new failure cache with the maximum size of
53       * {@link #DEFAULT_MAX_SIZE}.
54       */
55      public DefaultFailureCache() {
56          this(DEFAULT_MAX_SIZE);
57      }
58  
59      /**
60       * Creates a new failure cache with the specified maximum size.
61       * @param maxSize the maximum number of entries the cache should store
62       */
63      public DefaultFailureCache(final int maxSize) {
64          this.maxSize = maxSize;
65          this.storage = new ConcurrentHashMap<String, FailureCacheValue>();
66      }
67  
68      @Override
69      public int getErrorCount(final String identifier) {
70          if (identifier == null) {
71              throw new IllegalArgumentException("identifier may not be null");
72          }
73          final FailureCacheValue storedErrorCode = storage.get(identifier);
74          return storedErrorCode != null ? storedErrorCode.getErrorCount() : 0;
75      }
76  
77      @Override
78      public void resetErrorCount(final String identifier) {
79          if (identifier == null) {
80              throw new IllegalArgumentException("identifier may not be null");
81          }
82          storage.remove(identifier);
83      }
84  
85      @Override
86      public void increaseErrorCount(final String identifier) {
87          if (identifier == null) {
88              throw new IllegalArgumentException("identifier may not be null");
89          }
90          updateValue(identifier);
91          removeOldestEntryIfMapSizeExceeded();
92      }
93  
94      private void updateValue(final String identifier) {
95          /**
96           * Due to concurrency it is possible that someone else is modifying an
97           * entry before we could write back our updated value. So we keep
98           * trying until it is our turn.
99           *
100          * In case there is a lot of contention on that identifier, a thread
101          * might starve. Thus it gives up after a certain number of failed
102          * update tries.
103          */
104         for (int i = 0; i < MAX_UPDATE_TRIES; i++) {
105             final FailureCacheValue oldValue = storage.get(identifier);
106             if (oldValue == null) {
107                 final FailureCacheValue newValue = new FailureCacheValue(identifier, 1);
108                 if (storage.putIfAbsent(identifier, newValue) == null) {
109                     return;
110                 }
111             }
112             else {
113                 final int errorCount = oldValue.getErrorCount();
114                 if (errorCount == Integer.MAX_VALUE) {
115                     return;
116                 }
117                 final FailureCacheValue newValue = new FailureCacheValue(identifier, errorCount + 1);
118                 if (storage.replace(identifier, oldValue, newValue)) {
119                     return;
120                 }
121             }
122         }
123     }
124 
125     private void removeOldestEntryIfMapSizeExceeded() {
126         if (storage.size() > maxSize) {
127             final FailureCacheValue valueWithOldestTimestamp = findValueWithOldestTimestamp();
128             if (valueWithOldestTimestamp != null) {
129                 storage.remove(valueWithOldestTimestamp.getKey(), valueWithOldestTimestamp);
130             }
131         }
132     }
133 
134     private FailureCacheValue findValueWithOldestTimestamp() {
135         long oldestTimestamp = Long.MAX_VALUE;
136         FailureCacheValue oldestValue = null;
137         for (final Map.Entry<String, FailureCacheValue> storageEntry : storage.entrySet()) {
138             final FailureCacheValue value = storageEntry.getValue();
139             final long creationTimeInNanos = value.getCreationTimeInNanos();
140             if (creationTimeInNanos < oldestTimestamp) {
141                 oldestTimestamp = creationTimeInNanos;
142                 oldestValue = storageEntry.getValue();
143             }
144         }
145         return oldestValue;
146     }
147 }