• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19175

25 Apr 2024 10:38PM UTC coverage: 88.068% (-0.02%) from 88.091%
#19175

push

github

ejona86
rls: Fix time handling in CachingRlsLbClient

`getMinEvictionTime()` was fixed to make sure only deltas were used for
comparisons (`a < b` is broken; `a - b < 0` is okay). It had also
returned `0` by default, which was meaningless as there is no epoch for
`System.nanoTime()`. LinkedHashLruCache now passes the current time into
a few more functions since the implementations need it and it was
sometimes already available. This made it easier to make some classes
static.

31206 of 35434 relevant lines covered (88.07%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.12
/../rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java
1
/*
2
 * Copyright 2020 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.rls;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static com.google.common.base.Preconditions.checkState;
22

23
import com.google.common.base.MoreObjects;
24
import com.google.common.base.Ticker;
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.Iterator;
28
import java.util.LinkedHashMap;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.Objects;
32
import java.util.concurrent.ScheduledExecutorService;
33
import java.util.concurrent.ScheduledFuture;
34
import java.util.concurrent.TimeUnit;
35
import java.util.concurrent.atomic.AtomicLong;
36
import javax.annotation.CheckReturnValue;
37
import javax.annotation.Nullable;
38
import javax.annotation.concurrent.GuardedBy;
39
import javax.annotation.concurrent.ThreadSafe;
40

41
/**
42
 * A LinkedHashLruCache implements least recently used caching where it supports access order lru
43
 * cache eviction while allowing entry level expiration time. When the cache reaches max capacity,
44
 * LruCache try to remove up to one already expired entries. If it doesn't find any expired entries,
45
 * it will remove based on access order of entry. On top of this, LruCache also proactively removes
46
 * expired entries based on configured time interval.
47
 */
48
@ThreadSafe
49
abstract class LinkedHashLruCache<K, V> implements LruCache<K, V> {
50

51
  private final Object lock;
52

53
  @GuardedBy("lock")
54
  private final LinkedHashMap<K, SizedValue> delegate;
55
  private final PeriodicCleaner periodicCleaner;
56
  private final Ticker ticker;
57
  private final EvictionListener<K, SizedValue> evictionListener;
58
  private final AtomicLong estimatedSizeBytes = new AtomicLong();
1✔
59
  private long estimatedMaxSizeBytes;
60

61
  LinkedHashLruCache(
62
      final long estimatedMaxSizeBytes,
63
      @Nullable final EvictionListener<K, V> evictionListener,
64
      int cleaningInterval,
65
      TimeUnit cleaningIntervalUnit,
66
      ScheduledExecutorService ses,
67
      final Ticker ticker,
68
      Object lock) {
1✔
69
    checkState(estimatedMaxSizeBytes > 0, "max estimated cache size should be positive");
1✔
70
    this.estimatedMaxSizeBytes = estimatedMaxSizeBytes;
1✔
71
    this.lock = checkNotNull(lock, "lock");
1✔
72
    this.evictionListener = new SizeHandlingEvictionListener(evictionListener);
1✔
73
    this.ticker = checkNotNull(ticker, "ticker");
1✔
74
    delegate = new LinkedHashMap<K, SizedValue>(
1✔
75
        // rough estimate or minimum hashmap default
76
        Math.max((int) (estimatedMaxSizeBytes / 1000), 16),
1✔
77
        /* loadFactor= */ 0.75f,
78
        /* accessOrder= */ true) {
1✔
79
      @Override
80
      protected boolean removeEldestEntry(Map.Entry<K, SizedValue> eldest) {
81
        if (estimatedSizeBytes.get() <= LinkedHashLruCache.this.estimatedMaxSizeBytes) {
1✔
82
          return false;
1✔
83
        }
84

85
        // first, remove at most 1 expired entry
86
        boolean removed = cleanupExpiredEntries(1, ticker.read());
1✔
87
        // handles size based eviction if necessary no expired entry
88
        boolean shouldRemove = !removed
1✔
89
            && shouldInvalidateEldestEntry(eldest.getKey(), eldest.getValue().value, ticker.read());
1✔
90
        if (shouldRemove) {
1✔
91
          // remove entry by us to make sure lruIterator and cache is in sync
92
          LinkedHashLruCache.this.invalidate(eldest.getKey(), EvictionType.SIZE);
1✔
93
        }
94
        return false;
1✔
95
      }
96
    };
97
    periodicCleaner = new PeriodicCleaner(ses, cleaningInterval, cleaningIntervalUnit).start();
1✔
98
  }
1✔
99

100
  /**
101
   * Determines if the eldest entry should be kept or not when the cache size limit is reached. Note
102
   * that LruCache is access level and the eldest is determined by access pattern.
103
   */
104
  @SuppressWarnings("unused")
105
  protected boolean shouldInvalidateEldestEntry(K eldestKey, V eldestValue, long now) {
106
    return true;
1✔
107
  }
108

109
  /** Determines if the entry is already expired or not. */
110
  protected abstract boolean isExpired(K key, V value, long nowNanos);
111

112
  /**
113
   * Returns estimated size of entry to keep track. If it always returns 1, the max size bytes
114
   * behaves like max number of entry (default behavior).
115
   */
116
  @SuppressWarnings("unused")
117
  protected int estimateSizeOf(K key, V value) {
118
    return 1;
×
119
  }
120

121
  protected long estimatedMaxSizeBytes() {
122
    return estimatedMaxSizeBytes;
×
123
  }
124

125
  /** Updates size for given key if entry exists. It is useful if the cache value is mutated. */
126
  public void updateEntrySize(K key) {
127
    synchronized (lock) {
1✔
128
      SizedValue entry = readInternal(key);
1✔
129
      if (entry == null) {
1✔
130
        return;
×
131
      }
132
      int prevSize = entry.size;
1✔
133
      int newSize = estimateSizeOf(key, entry.value);
1✔
134
      entry.size = newSize;
1✔
135
      estimatedSizeBytes.addAndGet(newSize - prevSize);
1✔
136
    }
1✔
137
  }
1✔
138

139
  /**
140
   * Returns estimated cache size bytes. Each entry size is calculated by {@link
141
   * #estimateSizeOf(java.lang.Object, java.lang.Object)}.
142
   */
143
  public long estimatedSizeBytes() {
144
    return estimatedSizeBytes.get();
1✔
145
  }
146

147
  @Override
148
  @Nullable
149
  public final V cache(K key, V value) {
150
    checkNotNull(key, "key");
1✔
151
    checkNotNull(value, "value");
1✔
152
    SizedValue existing;
153
    int size = estimateSizeOf(key, value);
1✔
154
    synchronized (lock) {
1✔
155
      estimatedSizeBytes.addAndGet(size);
1✔
156
      existing = delegate.put(key, new SizedValue(size, value));
1✔
157
      if (existing != null) {
1✔
158
        evictionListener.onEviction(key, existing, EvictionType.REPLACED);
1✔
159
      }
160
    }
1✔
161
    return existing == null ? null : existing.value;
1✔
162
  }
163

164
  @Override
165
  @Nullable
166
  @CheckReturnValue
167
  public final V read(K key) {
168
    SizedValue entry = readInternal(key);
1✔
169
    if (entry != null) {
1✔
170
      return entry.value;
1✔
171
    }
172
    return null;
1✔
173
  }
174

175
  @Nullable
176
  @CheckReturnValue
177
  private SizedValue readInternal(K key) {
178
    checkNotNull(key, "key");
1✔
179
    synchronized (lock) {
1✔
180
      SizedValue existing = delegate.get(key);
1✔
181
      if (existing != null && isExpired(key, existing.value, ticker.read())) {
1✔
182
        return null;
×
183
      }
184
      return existing;
1✔
185
    }
186
  }
187

188
  @Override
189
  @Nullable
190
  public final V invalidate(K key) {
191
    return invalidate(key, EvictionType.EXPLICIT);
1✔
192
  }
193

194
  @Nullable
195
  private V invalidate(K key, EvictionType cause) {
196
    checkNotNull(key, "key");
1✔
197
    checkNotNull(cause, "cause");
1✔
198
    synchronized (lock) {
1✔
199
      SizedValue existing = delegate.remove(key);
1✔
200
      if (existing != null) {
1✔
201
        evictionListener.onEviction(key, existing, cause);
1✔
202
      }
203
      return existing == null ? null : existing.value;
1✔
204
    }
205
  }
206

207
  @Override
208
  public final void invalidateAll() {
209
    synchronized (lock) {
1✔
210
      Iterator<Map.Entry<K, SizedValue>> iterator = delegate.entrySet().iterator();
1✔
211
      while (iterator.hasNext()) {
1✔
212
        Map.Entry<K, SizedValue> entry = iterator.next();
1✔
213
        if (entry.getValue() != null) {
1✔
214
          evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.EXPLICIT);
1✔
215
        }
216
        iterator.remove();
1✔
217
      }
1✔
218
    }
1✔
219
  }
1✔
220

221
  @Override
222
  @CheckReturnValue
223
  public final boolean hasCacheEntry(K key) {
224
    // call readInternal to filter already expired entry in the cache
225
    return readInternal(key) != null;
1✔
226
  }
227

228
  /** Returns shallow copied values in the cache. */
229
  public final List<V> values() {
230
    synchronized (lock) {
1✔
231
      List<V> list = new ArrayList<>(delegate.size());
1✔
232
      for (SizedValue value : delegate.values()) {
1✔
233
        list.add(value.value);
1✔
234
      }
1✔
235
      return Collections.unmodifiableList(list);
1✔
236
    }
237
  }
238

239
  /**
240
   * Cleans up cache if needed to fit into max size bytes by
241
   * removing expired entries and removing oldest entries by LRU order.
242
   * Returns TRUE if any unexpired entries were removed
243
   */
244
  protected final boolean fitToLimit() {
245
    boolean removedAnyUnexpired = false;
1✔
246
    synchronized (lock) {
1✔
247
      if (estimatedSizeBytes.get() <= estimatedMaxSizeBytes) {
1✔
248
        // new size is larger no need to do cleanup
249
        return false;
1✔
250
      }
251
      // cleanup expired entries
252
      long now = ticker.read();
1✔
253
      cleanupExpiredEntries(now);
1✔
254

255
      // cleanup eldest entry until new size limit
256
      Iterator<Map.Entry<K, SizedValue>> lruIter = delegate.entrySet().iterator();
1✔
257
      while (lruIter.hasNext() && estimatedMaxSizeBytes < this.estimatedSizeBytes.get()) {
1✔
258
        Map.Entry<K, SizedValue> entry = lruIter.next();
1✔
259
        if (!shouldInvalidateEldestEntry(entry.getKey(), entry.getValue().value, now)) {
1✔
260
          break; // Violates some constraint like minimum age so stop our cleanup
×
261
        }
262
        lruIter.remove();
1✔
263
        // eviction listener will update the estimatedSizeBytes
264
        evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.SIZE);
1✔
265
        removedAnyUnexpired = true;
1✔
266
      }
1✔
267
    }
1✔
268
    return removedAnyUnexpired;
1✔
269
  }
270

271
  /**
272
   * Resizes cache. If new size is smaller than current estimated size, it will free up space by
273
   * removing expired entries and removing oldest entries by LRU order.
274
   */
275
  public final void resize(long newSizeBytes) {
276
    synchronized (lock) {
1✔
277
      this.estimatedMaxSizeBytes = newSizeBytes;
1✔
278
      fitToLimit();
1✔
279
    }
1✔
280
  }
1✔
281

282
  @Override
283
  @CheckReturnValue
284
  public final int estimatedSize() {
285
    synchronized (lock) {
1✔
286
      return delegate.size();
1✔
287
    }
288
  }
289

290
  private boolean cleanupExpiredEntries(long now) {
291
    return cleanupExpiredEntries(Integer.MAX_VALUE, now);
1✔
292
  }
293

294
  // maxExpiredEntries is by number of entries
295
  private boolean cleanupExpiredEntries(int maxExpiredEntries, long now) {
296
    checkArgument(maxExpiredEntries > 0, "maxExpiredEntries must be positive");
1✔
297
    boolean removedAny = false;
1✔
298
    synchronized (lock) {
1✔
299
      Iterator<Map.Entry<K, SizedValue>> lruIter = delegate.entrySet().iterator();
1✔
300
      while (lruIter.hasNext() && maxExpiredEntries > 0) {
1✔
301
        Map.Entry<K, SizedValue> entry = lruIter.next();
1✔
302
        if (isExpired(entry.getKey(), entry.getValue().value, now)) {
1✔
303
          lruIter.remove();
1✔
304
          evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.EXPIRED);
1✔
305
          removedAny = true;
1✔
306
          maxExpiredEntries--;
1✔
307
        }
308
      }
1✔
309
    }
1✔
310
    return removedAny;
1✔
311
  }
312

313
  @Override
314
  public final void close() {
315
    synchronized (lock) {
1✔
316
      periodicCleaner.stop();
1✔
317
      invalidateAll();
1✔
318
    }
1✔
319
  }
1✔
320

321
  /** Periodically cleans up the AsyncRequestCache. */
322
  private final class PeriodicCleaner {
323

324
    private final ScheduledExecutorService ses;
325
    private final int interval;
326
    private final TimeUnit intervalUnit;
327
    private ScheduledFuture<?> scheduledFuture;
328

329
    PeriodicCleaner(ScheduledExecutorService ses, int interval, TimeUnit intervalUnit) {
1✔
330
      this.ses = checkNotNull(ses, "ses");
1✔
331
      checkState(interval > 0, "interval must be positive");
1✔
332
      this.interval = interval;
1✔
333
      this.intervalUnit = checkNotNull(intervalUnit, "intervalUnit");
1✔
334
    }
1✔
335

336
    PeriodicCleaner start() {
337
      checkState(scheduledFuture == null, "cleaning task can be started only once");
1✔
338
      this.scheduledFuture =
1✔
339
          ses.scheduleAtFixedRate(new CleaningTask(), interval, interval, intervalUnit);
1✔
340
      return this;
1✔
341
    }
342

343
    void stop() {
344
      if (scheduledFuture != null) {
1✔
345
        scheduledFuture.cancel(false);
1✔
346
        scheduledFuture = null;
1✔
347
      }
348
    }
1✔
349

350
    private class CleaningTask implements Runnable {
1✔
351

352
      @Override
353
      public void run() {
354
        cleanupExpiredEntries(ticker.read());
1✔
355
      }
1✔
356
    }
357
  }
358

359
  /** A {@link EvictionListener} keeps track of size. */
360
  private final class SizeHandlingEvictionListener implements EvictionListener<K, SizedValue> {
361

362
    private final EvictionListener<K, V> delegate;
363

364
    SizeHandlingEvictionListener(@Nullable EvictionListener<K, V> delegate) {
1✔
365
      this.delegate = delegate;
1✔
366
    }
1✔
367

368
    @Override
369
    public void onEviction(K key, SizedValue value, EvictionType cause) {
370
      estimatedSizeBytes.addAndGet(-1L * value.size);
1✔
371
      if (delegate != null) {
1✔
372
        delegate.onEviction(key, value.value, cause);
1✔
373
      }
374
    }
1✔
375
  }
376

377
  private final class SizedValue {
378
    volatile int size;
379
    final V value;
380

381
    SizedValue(int size, V value) {
1✔
382
      this.size = size;
1✔
383
      this.value = value;
1✔
384
    }
1✔
385

386
    @Override
387
    public boolean equals(Object o) {
388
      // NOTE: the size doesn't affect equality
389
      if (this == o) {
×
390
        return true;
×
391
      }
392
      if (o == null || getClass() != o.getClass()) {
×
393
        return false;
×
394
      }
395
      LinkedHashLruCache<?, ?>.SizedValue that = (LinkedHashLruCache<?, ?>.SizedValue) o;
×
396
      return Objects.equals(value, that.value);
×
397
    }
398

399
    @Override
400
    public int hashCode() {
401
      // NOTE: the size doesn't affect hashCode
402
      return Objects.hash(value);
×
403
    }
404

405
    @Override
406
    public String toString() {
407
      return MoreObjects.toStringHelper(this)
×
408
          .add("size", size)
×
409
          .add("value", value)
×
410
          .toString();
×
411
    }
412
  }
413
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc