summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/cache/PerThreadCache.java
blob: 459650d90c382fcbbc4b107ffc0f0b96a72905e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.cache;

import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;

/**
 * Caches object instances for a request as {@link ThreadLocal} in the serving thread.
 *
 * <p>This class is intended to cache objects that have a high instantiation cost, are specific to
 * the current request and potentially need to be instantiated multiple times while serving a
 * request.
 *
 * <p>This is different from the key-value storage in {@code CurrentUser}: {@code CurrentUser}
 * offers a key-value storage by providing thread-safe {@code get} and {@code put} methods. Once the
 * value is retrieved through {@code get} there is not thread-safety anymore - apart from the
 * retrieved object guarantees. Depending on the implementation of {@code CurrentUser}, it might be
 * shared between the request serving thread as well as sub- or background treads.
 *
 * <p>In comparison to that, this class guarantees thread safety even on non-thread-safe objects as
 * its cache is tied to the serving thread only. While allowing to cache non-thread-safe objects, it
 * has the downside of not sharing any objects with background threads or executors.
 *
 * <p>Lastly, this class offers a cache, that requires callers to also provide a {@code Supplier} in
 * case the object is not present in the cache, while {@code CurrentUser} provides a storage where
 * just retrieving stored values is a valid operation.
 *
 * <p>To prevent OOM errors on requests that would cache a lot of objects, this class enforces an
 * internal limit after which no new elements are cached. All {@code get} calls are served by
 * invoking the {@code Supplier} after that.
 */
public class PerThreadCache implements AutoCloseable {
  private static final ThreadLocal<PerThreadCache> CACHE = new ThreadLocal<>();
  /**
   * Cache at maximum 25 values per thread. This value was chosen arbitrarily. Some endpoints (like
   * ListProjects) break the assumption that the data cached in a request is limited. To prevent
   * this class from accumulating an unbound number of objects, we enforce this limit.
   */
  private static final int PER_THREAD_CACHE_SIZE = 25;

  /**
   * True when the current thread is associated with an incoming API request that is not changing
   * any state.
   */
  private final boolean readOnlyRequest;

  private final Map<Key<?>, Consumer<Object>> unloaders =
      Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE);

  /**
   * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
   * class and a list of identifiers that in combination uniquely set the object apart form others
   * of the same class.
   */
  public static final class Key<T> {
    private final Class<T> clazz;
    private final ImmutableList<Object> identifiers;

    /**
     * Returns a key based on the value's class and an identifier that uniquely identify the value.
     * The identifier needs to implement {@code equals()} and {@hashCode()}.
     */
    public static <T> Key<T> create(Class<T> clazz, Object identifier) {
      return new Key<>(clazz, ImmutableList.of(identifier));
    }

    /**
     * Returns a key based on the value's class and a set of identifiers that uniquely identify the
     * value. Identifiers need to implement {@code equals()} and {@hashCode()}.
     */
    public static <T> Key<T> create(Class<T> clazz, Object... identifiers) {
      return new Key<>(clazz, ImmutableList.copyOf(identifiers));
    }

    private Key(Class<T> clazz, ImmutableList<Object> identifiers) {
      this.clazz = clazz;
      this.identifiers = identifiers;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(clazz, identifiers);
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof Key)) {
        return false;
      }
      Key<?> other = (Key<?>) o;
      return this.clazz == other.clazz && this.identifiers.equals(other.identifiers);
    }
  }

  /**
   * Creates a thread-local cache associated to an incoming HTTP request.
   *
   * <p>The request is considered as read-only if the associated method is GET or HEAD.
   *
   * @param httpRequest HTTP request associated with the thread-local cache
   * @return thread-local cache
   */
  public static PerThreadCache create(@Nullable HttpServletRequest httpRequest) {
    checkState(CACHE.get() == null, "called create() twice on the same request");
    PerThreadCache cache = new PerThreadCache(httpRequest, false);
    CACHE.set(cache);
    return cache;
  }

  /**
   * Creates a thread-local cache associated to an incoming read-only request.
   *
   * @return thread-local cache
   */
  public static PerThreadCache createReadOnly() {
    checkState(CACHE.get() == null, "called create() twice on the same request");
    PerThreadCache cache = new PerThreadCache(null, true);
    CACHE.set(cache);
    return cache;
  }

  @Nullable
  public static PerThreadCache get() {
    return CACHE.get();
  }

  /**
   * Return a cached value associated with a key fetched with a loader and released with an unloader
   * function.
   *
   * @param <T> The data type of the cached value
   * @param key the key associated with the value
   * @param loader the loader function for fetching the value from the key
   * @param unloader the unloader function for releasing the value when unloaded from the cache
   * @return Optional of the cached value or empty if the value could not be cached for any reason
   *     (e.g. cache full)
   */
  public static <T> Optional<T> get(Key<T> key, Supplier<T> loader, Consumer<T> unloader) {
    return Optional.ofNullable(get()).flatMap(c -> c.getWithLoader(key, loader, unloader));
  }

  /**
   * Legacy way for retrieving a cached element through a loader.
   *
   * <p>This method is deprecated because it was error-prone due to the unclear ownership of the
   * objects created through the loader. When the cache has space available, the entries are loaded
   * and cached, hence owned and reused by the cache.
   *
   * <p>When the cache is full, this method just short-circuit to the invocation of the loader and
   * the objects created aren't owned or stored by the cache, leaving the space for potential memory
   * and resources leaks.
   *
   * <p>Because of the unclear semantics of the method (who owns the instances? are they reused?)
   * this is now deprecated the the caller should use instead the {@link PerThreadCache#get(Key,
   * Supplier, Consumer)} which has a clear ownership policy.
   *
   * @deprecated use {@link PerThreadCache#get(Key, Supplier, Consumer)}
   */
  public static <T> T getOrCompute(Key<T> key, Supplier<T> loader) {
    PerThreadCache cache = get();
    return cache != null ? cache.get(key, loader) : loader.get();
  }

  private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE);

  private PerThreadCache(@Nullable HttpServletRequest req, boolean readOnly) {
    readOnlyRequest =
        readOnly
            || (req != null
                && (req.getMethod().equalsIgnoreCase("GET")
                    || req.getMethod().equalsIgnoreCase("HEAD")));
  }

  /**
   * Legacy way of retrieving an instance of {@code T} that was either loaded from the cache or
   * obtained from the provided {@link Supplier}.
   *
   * <p>This method is deprecated because it was error-prone due to the unclear ownership of the
   * objects created through the loader. When the cache has space available, the entries are loaded
   * and cached, hence owned and reused by the cache.
   *
   * <p>When the cache is full, this method just short-circuit to the invocation of the loader and
   * the objects created aren't owned or stored by the cache, leaving the space for potential memory
   * and resources leaks.
   *
   * <p>Because of the unclear semantics of the method (who owns the instances? are they reused?)
   * this is now deprecated the the caller should use instead the {@link PerThreadCache#get(Key,
   * Supplier, Consumer)} which has a clear ownership policy.
   *
   * @deprecated use {@link PerThreadCache#getWithLoader(Key, Supplier, Consumer)}
   */
  public <T> T get(Key<T> key, Supplier<T> loader) {
    return getWithLoader(key, loader, null).orElse(loader.get());
  }

  @SuppressWarnings("unchecked")
  public <T> Optional<T> getWithLoader(
      Key<T> key, Supplier<T> loader, @Nullable Consumer<T> unloader) {
    T value = (T) cache.get(key);
    if (value == null && cache.size() < PER_THREAD_CACHE_SIZE) {
      value = loader.get();
      cache.put(key, value);
      if (unloader != null) {
        unloaders.put(key, (Consumer<Object>) unloader);
      }
    }
    return Optional.ofNullable(value);
  }

  /** Returns true if the associated request is read-only */
  public boolean hasReadonlyRequest() {
    return readOnlyRequest;
  }

  @Override
  public void close() {
    Optional.of(CACHE.get())
        .map(v -> v.unloaders.entrySet().stream())
        .orElse(Stream.empty())
        .forEach(this::unload);

    CACHE.remove();
  }

  private <T> void unload(Entry<Key<?>, Consumer<Object>> unloaderEntry) {
    unloaderEntry.getValue().accept(cache.get(unloaderEntry.getKey()));
  }
}