Skip to content

Commit 344a8fc

Browse files
committed
Simplify SanityCache
1 parent fefa40b commit 344a8fc

3 files changed

Lines changed: 40 additions & 72 deletions

File tree

src/main/java/org/kohsuke/github/GitHubClient.java

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -953,32 +953,30 @@ String getLogin() {
953953
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
954954
// Even when explicitly asking for rate limit, restrict to sane query frequency
955955
// return cached value if available
956-
GHRateLimit output = sanityCachedRateLimit.get(
957-
(currentValue) -> currentValue == null || currentValue.getRecord(rateLimitTarget).isExpired(),
958-
() -> {
959-
GHRateLimit result;
960-
try {
961-
final GitHubRequest request = GitHubRequest.newBuilder()
962-
.rateLimit(RateLimitTarget.NONE)
963-
.withApiUrl(getApiUrl())
964-
.withUrlPath("/rate_limit")
965-
.build();
966-
result = this
967-
.sendRequest(request,
968-
(connectorResponse) -> GitHubResponse.parseBody(connectorResponse,
969-
JsonRateLimit.class))
970-
.body().resources;
971-
} catch (FileNotFoundException e) {
972-
// For some versions of GitHub Enterprise, the rate_limit endpoint returns a 404.
973-
LOGGER.log(FINE, "(%s) /rate_limit returned 404 Not Found.", sendRequestTraceId.get());
974-
975-
// However some newer versions of GHE include rate limit header information
976-
// If the header info is missing and the endpoint returns 404, fill the rate limit
977-
// with unknown
978-
result = GHRateLimit.fromRecord(GHRateLimit.UnknownLimitRecord.current(), rateLimitTarget);
979-
}
980-
return result;
981-
});
956+
GHRateLimit output = sanityCachedRateLimit.get(() -> {
957+
GHRateLimit result;
958+
try {
959+
final GitHubRequest request = GitHubRequest.newBuilder()
960+
.rateLimit(RateLimitTarget.NONE)
961+
.withApiUrl(getApiUrl())
962+
.withUrlPath("/rate_limit")
963+
.build();
964+
result = this
965+
.sendRequest(request,
966+
(connectorResponse) -> GitHubResponse.parseBody(connectorResponse,
967+
JsonRateLimit.class))
968+
.body().resources;
969+
} catch (FileNotFoundException e) {
970+
// For some versions of GitHub Enterprise, the rate_limit endpoint returns a 404.
971+
LOGGER.log(FINE, "(%s) /rate_limit returned 404 Not Found.", sendRequestTraceId.get());
972+
973+
// However some newer versions of GHE include rate limit header information
974+
// If the header info is missing and the endpoint returns 404, fill the rate limit
975+
// with unknown
976+
result = GHRateLimit.fromRecord(GHRateLimit.UnknownLimitRecord.current(), rateLimitTarget);
977+
}
978+
return result;
979+
});
982980
return updateRateLimit(output);
983981
}
984982

src/main/java/org/kohsuke/github/GitHubSanityCachedValue.java

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import java.time.Instant;
66
import java.util.concurrent.locks.Lock;
77
import java.util.concurrent.locks.ReentrantReadWriteLock;
8-
import java.util.function.Function;
98

109
/**
1110
* GitHubSanityCachedValue limits queries for a particular value to once per second.
@@ -22,29 +21,24 @@ class GitHubSanityCachedValue<T> {
2221
/**
2322
* Gets the value from the cache or calls the supplier if the cache is empty or out of date.
2423
*
25-
* @param isExpired
26-
* a supplier that returns true if the cached value is no longer valid.
2724
* @param query
2825
* a supplier the returns an updated value. Only called if the cache is empty or out of date.
2926
* @return the value from the cache or the value returned from the supplier.
3027
* @throws E
3128
* the exception thrown by the supplier if it fails.
3229
*/
33-
<E extends Throwable> T get(Function<T, Boolean> isExpired, SupplierThrows<T, E> query) throws E {
30+
<E extends Throwable> T get(SupplierThrows<T, E> query) throws E {
3431
readLock.lock();
3532
try {
36-
boolean expired = Instant.now().getEpochSecond() > lastQueriedAtEpochSeconds || isExpired.apply(lastResult);
37-
if (!expired) {
33+
if (Instant.now().getEpochSecond() <= lastQueriedAtEpochSeconds) {
3834
return lastResult;
3935
}
4036
} finally {
4137
readLock.unlock();
4238
}
4339
writeLock.lock();
4440
try {
45-
boolean stillExpired = Instant.now().getEpochSecond() > lastQueriedAtEpochSeconds
46-
|| isExpired.apply(lastResult);
47-
if (stillExpired) {
41+
if (Instant.now().getEpochSecond() > lastQueriedAtEpochSeconds) {
4842
lastResult = query.get();
4943
lastQueriedAtEpochSeconds = Instant.now().getEpochSecond();
5044
}
@@ -53,17 +47,4 @@ <E extends Throwable> T get(Function<T, Boolean> isExpired, SupplierThrows<T, E>
5347
writeLock.unlock();
5448
}
5549
}
56-
57-
/**
58-
* Gets the value from the cache or calls the supplier if the cache is empty or out of date.
59-
*
60-
* @param query
61-
* a supplier the returns an updated value. Only called if the cache is empty or out of date.
62-
* @return the value from the cache or the value returned from the supplier.
63-
* @throws E
64-
* the exception thrown by the supplier if it fails.
65-
*/
66-
<E extends Throwable> T get(SupplierThrows<T, E> query) throws E {
67-
return get((value) -> Boolean.FALSE, query);
68-
}
6950
}

src/test/java/org/kohsuke/github/GitHubSanityCachedValueTest.java

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void concurrentCallersOnlyRefreshOnce() throws Exception {
7373
try {
7474
ready.countDown();
7575
start.await();
76-
String value = cachedValue.get((result) -> result == null, () -> {
76+
String value = cachedValue.get(() -> {
7777
calls.incrementAndGet();
7878
return "value";
7979
});
@@ -100,39 +100,28 @@ public void concurrentCallersOnlyRefreshOnce() throws Exception {
100100
}
101101

102102
/**
103-
* Tests that the {@code isExpired} predicate alone can force a cache refresh even when the cached value is still
104-
* current within the same second. This exercises the branch where the time-check condition ({@code A}) evaluates to
105-
* {@code false} but the {@code isExpired} predicate ({@code B}) evaluates to {@code true}, covering the
106-
* {@code A=false, B=true} path in both the read-lock check and the write-lock double-check inside
107-
* {@code GitHubSanityCachedValue}.
103+
* Tests that a result which is already expired on arrival — for example, the
104+
* {@code GHRateLimit.UnknownLimitRecord} returned when a GitHub Enterprise {@code /rate_limit}
105+
* endpoint responds with 404 — is still held for one second. Without the time-based TTL,
106+
* re-checking expiry immediately after a refresh would cause every subsequent call to re-query,
107+
* creating a query storm.
108108
*
109109
* @throws Exception
110110
* if the test fails
111111
*/
112112
@Test
113-
public void isExpiredPredicateTriggersRefreshWithinSameSecond() throws Exception {
113+
public void doesNotReQueryWhenResultIsAlreadyExpiredOnArrival() throws Exception {
114114
alignToStartOfSecond();
115115
GitHubSanityCachedValue<String> cachedValue = new GitHubSanityCachedValue<>();
116116
AtomicInteger calls = new AtomicInteger();
117117

118-
// Populate the cache within the current second using an isExpired predicate that never
119-
// expires on its own.
120-
String first = cachedValue.get(result -> false, () -> {
121-
calls.incrementAndGet();
122-
return "stale";
123-
});
118+
// Supplier always returns a value that an isExpired() check would immediately reject,
119+
// e.g. GHRateLimit.UnknownLimitRecord when GitHub Enterprise returns 404 for /rate_limit.
120+
cachedValue.get(() -> { calls.incrementAndGet(); return "expired-on-arrival"; });
121+
cachedValue.get(() -> { calls.incrementAndGet(); return "expired-on-arrival"; });
122+
cachedValue.get(() -> { calls.incrementAndGet(); return "expired-on-arrival"; });
124123

125-
// Within the same second, pass an isExpired predicate that always returns true. This forces
126-
// re-evaluation through the write lock even though the time has not elapsed, covering the
127-
// A=false, B=true branch in both compound conditions.
128-
String second = cachedValue.get(result -> true, () -> {
129-
calls.incrementAndGet();
130-
return "fresh";
131-
});
132-
133-
assertThat(first, equalTo("stale"));
134-
assertThat(second, equalTo("fresh"));
135-
assertThat(calls.get(), equalTo(2));
124+
assertThat(calls.get(), equalTo(1));
136125
}
137126

138127
/**

0 commit comments

Comments
 (0)