View Javadoc
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          // Without rotation this must still be 0
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          // rotating without any data should not change the latency
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              // This rotates 10 times which is not enough to trigger dropping early data.
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         // The histogram should be completely removed now
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 }