-
Notifications
You must be signed in to change notification settings - Fork 775
Expand file tree
/
Copy pathGitHubSanityCachedValueTest.java
More file actions
166 lines (145 loc) · 5.58 KB
/
GitHubSanityCachedValueTest.java
File metadata and controls
166 lines (145 loc) · 5.58 KB
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
package org.kohsuke.github;
import org.junit.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* The Class GitHubSanityCachedValueTest.
*/
public class GitHubSanityCachedValueTest {
private static void alignToStartOfSecond() {
while (Instant.now().getNano() > 100_000_000) {
Thread.yield();
}
}
/**
* Tests that the cache returns the same value without querying again when accessed multiple times within the same
* second.
*
* @throws Exception
* if the test fails
*/
@Test
public void cachesWithinSameSecond() throws Exception {
alignToStartOfSecond();
GitHubSanityCachedValue<String> cachedValue = new GitHubSanityCachedValue<>();
AtomicInteger calls = new AtomicInteger();
String first = cachedValue.get(() -> {
calls.incrementAndGet();
return "value";
});
String second = cachedValue.get(() -> {
calls.incrementAndGet();
return "value";
});
assertThat(first, equalTo("value"));
assertThat(second, equalTo("value"));
assertThat(calls.get(), equalTo(1));
}
/**
* Tests that multiple concurrent callers only trigger a single refresh of the cached value, preventing redundant
* queries.
*
* @throws Exception
* if the test fails
*/
@Test
public void concurrentCallersOnlyRefreshOnce() throws Exception {
alignToStartOfSecond();
GitHubSanityCachedValue<String> cachedValue = new GitHubSanityCachedValue<>();
AtomicInteger calls = new AtomicInteger();
List<String> results = Collections.synchronizedList(new ArrayList<>());
CountDownLatch ready = new CountDownLatch(5);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch finished = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
try {
ready.countDown();
start.await();
String value = cachedValue.get((result) -> result == null, () -> {
calls.incrementAndGet();
return "value";
});
results.add(value);
} catch (Exception ignored) {
results.add(null);
} finally {
finished.countDown();
}
});
thread.start();
}
ready.await();
start.countDown();
finished.await();
assertThat(calls.get(), equalTo(1));
assertThat(results.size(), equalTo(5));
for (String result : results) {
assertThat(result, notNullValue());
assertThat(result, equalTo("value"));
}
}
/**
* Tests that the {@code isExpired} predicate alone can force a cache refresh even when the cached value is still
* current within the same second. This exercises the branch where the time-check condition ({@code A}) evaluates to
* {@code false} but the {@code isExpired} predicate ({@code B}) evaluates to {@code true}, covering the
* {@code A=false, B=true} path in both the read-lock check and the write-lock double-check inside
* {@code GitHubSanityCachedValue}.
*
* @throws Exception
* if the test fails
*/
@Test
public void isExpiredPredicateTriggersRefreshWithinSameSecond() throws Exception {
alignToStartOfSecond();
GitHubSanityCachedValue<String> cachedValue = new GitHubSanityCachedValue<>();
AtomicInteger calls = new AtomicInteger();
// Populate the cache within the current second using an isExpired predicate that never
// expires on its own.
String first = cachedValue.get(result -> false, () -> {
calls.incrementAndGet();
return "stale";
});
// Within the same second, pass an isExpired predicate that always returns true. This forces
// re-evaluation through the write lock even though the time has not elapsed, covering the
// A=false, B=true branch in both compound conditions.
String second = cachedValue.get(result -> true, () -> {
calls.incrementAndGet();
return "fresh";
});
assertThat(first, equalTo("stale"));
assertThat(second, equalTo("fresh"));
assertThat(calls.get(), equalTo(2));
}
/**
* Tests that the cache is refreshed after one second has elapsed, triggering a new query to retrieve the updated
* value.
*
* @throws Exception
* if the test fails
*/
@Test
public void refreshesAfterOneSecond() throws Exception {
GitHubSanityCachedValue<String> cachedValue = new GitHubSanityCachedValue<>();
AtomicInteger calls = new AtomicInteger();
String first = cachedValue.get(() -> {
calls.incrementAndGet();
return "value";
});
Thread.sleep(1100);
String second = cachedValue.get(() -> {
calls.incrementAndGet();
return "value";
});
assertThat(first, equalTo("value"));
assertThat(second, equalTo("value"));
assertThat(calls.get(), equalTo(2));
}
}