1 package org.wikimedia.search.extra.latency;
2
3 import static org.hamcrest.Matchers.greaterThan;
4 import static org.junit.Assert.assertEquals;
5 import static org.junit.Assert.assertNotNull;
6 import static org.junit.Assert.assertThat;
7 import static org.mockito.Mockito.mock;
8 import static org.mockito.Mockito.when;
9
10 import java.util.Collections;
11 import java.util.LinkedList;
12 import java.util.List;
13 import java.util.Set;
14
15 import org.elasticsearch.common.unit.TimeValue;
16 import org.elasticsearch.search.internal.SearchContext;
17 import org.junit.Test;
18 import org.wikimedia.search.extra.util.Suppliers.MutableSupplier;
19
20 import com.carrotsearch.randomizedtesting.RandomizedTest;
21
22 public class SearchLatencyListenerTest extends RandomizedTest {
23 @Test
24 public void startsWithNoBuckets() {
25 SearchLatencyListener listener = newListener();
26 assertEquals(0, listener.getLatencyStats(Collections.singleton(42D)).size());
27 }
28
29 @Test
30 public void onQueryPhaseAddsBucket() {
31 SearchLatencyListener listener = newListener();
32 listener.onQueryPhase(mockSearchContext(Collections.singletonList("foo")), 999);
33 assertEquals(1, listener.getLatencyStats(Collections.singleton(99D)).size());
34 }
35
36 @Test
37 public void acceptsValuesSmallerThanMinimum() {
38 SearchLatencyListener listener = newListener();
39 listener.onQueryPhase(mockSearchContext(Collections.singletonList("foo")), TimeValue.NSEC_PER_MSEC);
40 listener.rotate();
41 assertThat(getMillisAtPercentile(listener, "foo", 99D), greaterThan(0D));
42 }
43
44 @Test
45 public void acceptsValuesLargerThanMaximum() {
46 SearchLatencyListener listener = newListener();
47 long tookInNanos = TimeValue.timeValueHours(2).nanos();
48 listener.onQueryPhase(mockSearchContext(Collections.singletonList("foo")), tookInNanos);
49 listener.rotate();
50 assertThat(getMillisAtPercentile(listener, "foo", 99D), greaterThan(0D));
51 }
52
53 @Test
54 public void rotate() {
55 final Set<Double> latencies = Collections.singleton(95D);
56 SearchLatencyListener listener = newListener();
57 long tookInNanos = 12345678;
58 listener.onQueryPhase(mockSearchContext(Collections.singletonList("foo")), tookInNanos);
59
60 assertEquals(1, listener.getLatencyStats(latencies).size());
61 SearchLatencyProbe.LatencyStat stat = listener.getLatencyStats(latencies).get(0);
62 assertNotNull(stat);
63 assertEquals("foo", stat.getBucket());
64 assertEquals(95D, stat.getPercentile(), Math.ulp(95D));
65
66 assertEquals(0D, stat.getLatency().nanos(), Math.ulp(0D));
67
68 listener.rotate();
69 assertEquals(1, listener.getLatencyStats(latencies).size());
70 stat = listener.getLatencyStats(latencies).get(0);
71 assertNotNull(stat);
72 assertEquals("foo", stat.getBucket());
73 assertEquals(95D, stat.getPercentile(), Math.ulp(95D));
74 assertEquals(tookInNanos, stat.getLatency().nanos(), delta(tookInNanos));
75
76 listener.rotate();
77 assertEquals(1, listener.getLatencyStats(latencies).size());
78 stat = listener.getLatencyStats(latencies).get(0);
79 assertNotNull(stat);
80 assertEquals("foo", stat.getBucket());
81 assertEquals(95D, stat.getPercentile(), Math.ulp(95D));
82
83 assertEquals(tookInNanos, stat.getLatency().nanos(), delta(tookInNanos));
84 }
85
86 @Test
87 public void histogramIsApproximatelyCorrect() {
88 SearchLatencyListener listener = newListener();
89 SearchContext context = mockSearchContext(Collections.singletonList("foo"));
90 List<Long> values = new LinkedList<>();
91 int max = randomIntBetween(1000, 10000);
92 for (int i = 0; i < 2000; i++) {
93 long tookInNanos = randomLongBetween(TimeValue.NSEC_PER_MSEC, max * TimeValue.NSEC_PER_MSEC);
94 values.add(tookInNanos);
95 listener.onQueryPhase(context, tookInNanos);
96
97 if (i % 200 == 0) {
98 listener.rotate();
99 }
100 }
101
102 double expectedMs = values.stream().sorted().skip(1900).findFirst().orElseThrow(AssertionError::new) / TimeValue.NSEC_PER_MSEC;
103 assertEquals(expectedMs, getMillisAtPercentile(listener, "foo", 95D), delta(expectedMs));
104 }
105
106 @Test
107 public void rotationDropsOldData() {
108 SearchLatencyListener listener = newListener();
109 SearchContext context = mockSearchContext(Collections.singletonList("baz"));
110
111 TimeValue tv = TimeValue.timeValueMillis(123);
112 listener.onQueryPhase(context, tv.nanos());
113 listener.rotate();
114 TimeValue tv2 = TimeValue.timeValueMillis(12345);
115 listener.onQueryPhase(context, tv2.nanos());
116
117 for (int i = 1; i < SearchLatencyListener.NUM_ROLLING_HISTOGRAMS; i++) {
118 assertEquals(tv.millis(), getMillisAtPercentile(listener, "baz", 0D), delta(tv.millis()));
119 listener.rotate();
120 }
121
122 assertEquals(tv.millis(), getMillisAtPercentile(listener, "baz", 0D), delta(tv.millis()));
123 listener.rotate();
124 assertEquals(tv2.millis(), getMillisAtPercentile(listener, "baz", 0D), delta(tv2.millis()));
125 assertEquals(1, listener.getLatencyStats(Collections.singleton(0D)).size());
126 listener.rotate();
127
128 assertEquals(0, listener.getLatencyStats(Collections.singleton(0D)).size());
129 }
130
131 private double delta(double val) {
132 return val * (1 / (10D * SearchLatencyListener.SIGNIFICANT_DIGITS));
133 }
134
135 private SearchLatencyListener newListener() {
136 return new SearchLatencyListener(new MutableSupplier<>());
137 }
138
139 private SearchContext mockSearchContext(List<String> buckets) {
140 SearchContext context = mock(SearchContext.class);
141 when(context.groupStats()).thenReturn(buckets);
142 return context;
143 }
144
145 private double getMillisAtPercentile(SearchLatencyProbe probe, String bucket, double percentile) {
146 return probe.getLatencyStats(Collections.singleton(percentile)).stream()
147 .filter(stat -> stat.getBucket().equals(bucket))
148 .findFirst()
149 .orElseThrow(() -> new IllegalArgumentException("Bucket not returned by latency probe."))
150 .getLatency().millis();
151 }
152 }