aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/test/java/com/yahoo/search/searchchain/test/TraceTestCase.java
blob: f2c8551126b1ec185c7e80d4c54146d1ecf1c6f0 (plain) (blame)
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.searchchain.test;

import com.yahoo.component.ComponentId;
import com.yahoo.component.chain.Chain;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.result.Hit;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.yolean.trace.TraceNode;
import com.yahoo.yolean.trace.TraceVisitor;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

/**
 * Tests tracing scenarios where traces from multiple executions over the same query are involved.
 *
 * @author bratseth
 */
public class TraceTestCase {

    @Test
    void testTracingOnCorrectAPIUseNonParallel() {
        assertTracing(true, false);
    }

    @Test
    void testTracingOnIncorrectAPIUseNonParallel() {
        assertTracing(false, false);
    }

    @Test
    void testTracingOnCorrectAPIUseParallel() {
        assertTracing(true, true);
    }

    @Test
    void testTracingOnIncorrectAPIUseParallel() {
        assertTracing(false, true);
    }

    @Test
    void testTraceWithQuery() {
        testQueryInTrace(true, "trace.query=true");
        testQueryInTrace(false, "trace.query=false");
        testQueryInTrace(true, "");
    }

    private void testQueryInTrace(boolean expectQueryInTrace, String queryParameters) {
        Query query = new Query("?query=foo&trace.level=1&" + queryParameters);
        Chain<Searcher> chain = new Chain<>(new Tracer("tracer1", true));
        Execution execution = new Execution(chain, Execution.Context.createContextStub());
        Result result = execution.search(query);
        Iterator<String> trace = collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        assertEquals("  (level start)", trace.next());
        if (expectQueryInTrace)
            assertEquals("    During tracer1: 0: [WEAKAND(100) foo]", trace.next());
        else
            assertEquals("    During tracer1: 0", trace.next());
    }

    @Test
    void testTraceInvocationsUnfillableHits() {
        final int traceLevel = 5;
        Query query = new Query("?trace.level=" + traceLevel);
        Chain<Searcher> forkingChain = new Chain<>(new Tracer("tracer1"),
                new Tracer("tracer2"),
                new Backend("backend1", false));
        Execution execution = new Execution(forkingChain, Execution.Context.createContextStub());
        Result result = execution.search(query);
        execution.fill(result, "mySummary");

        Iterator<String> trace = collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        trace.next(); // (properties trace: not checked)
        assertEquals("  (level start)", trace.next());
        assertEquals("    Invoke searcher 'tracer1'", trace.next());
        assertEquals("    During tracer1: 0", trace.next());
        assertEquals("    Invoke searcher 'tracer2'", trace.next());
        assertEquals("    During tracer2: 0", trace.next());
        assertEquals("    Invoke searcher 'backend1'", trace.next());
        assertEquals("    Return searcher 'backend1'", trace.next());
        assertEquals("    Return searcher 'tracer2'", trace.next());
        assertEquals("    Return searcher 'tracer1'", trace.next());
        assertEquals("    Invoke fill(mySummary) on searcher 'tracer1'", trace.next());
        assertEquals("    Ignoring fill(mySummary): Hits are unfillable: result.hits().getFilled()=null", trace.next());
        assertEquals("    Return fill(mySummary) on searcher 'tracer1'", trace.next());
        assertEquals("  (level end)", trace.next());
        assertEquals("(level end)", trace.next());
        assertFalse(trace.hasNext());
    }

    @Test
    void testTraceInvocationsFillableHits() {
        final int traceLevel = 5;
        Query query = new Query("?tracelevel=" + traceLevel);
        Chain<Searcher> forkingChain = new Chain<>(new Tracer("tracer1"),
                new Tracer("tracer2"),
                new Backend("backend1", true));
        Execution execution = new Execution(forkingChain, Execution.Context.createContextStub());
        Result result = execution.search(query);
        execution.fill(result, "mySummary");

        Iterator<String> trace = collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        trace.next(); // (properties trace: not checked)
        assertEquals("  (level start)", trace.next());
        assertEquals("    Invoke searcher 'tracer1'", trace.next());
        assertEquals("    During tracer1: 0", trace.next());
        assertEquals("    Invoke searcher 'tracer2'", trace.next());
        assertEquals("    During tracer2: 0", trace.next());
        assertEquals("    Invoke searcher 'backend1'", trace.next());
        assertEquals("    Return searcher 'backend1'", trace.next());
        assertEquals("    Return searcher 'tracer2'", trace.next());
        assertEquals("    Return searcher 'tracer1'", trace.next());
        assertEquals("    Invoke fill(mySummary) on searcher 'tracer1'", trace.next());
        assertEquals("    Invoke fill(mySummary) on searcher 'tracer2'", trace.next());
        assertEquals("    Invoke fill(mySummary) on searcher 'backend1'", trace.next());
        assertEquals("    Return fill(mySummary) on searcher 'backend1'", trace.next());
        assertEquals("    Return fill(mySummary) on searcher 'tracer2'", trace.next());
        assertEquals("    Return fill(mySummary) on searcher 'tracer1'", trace.next());
        assertEquals("  (level end)", trace.next());
        assertEquals("(level end)", trace.next());
        assertFalse(trace.hasNext());
    }

    private void assertTracing(boolean carryOverContext, boolean parallel) {
        Query query = new Query("?tracelevel=1");
        assertEquals(1, query.getTrace().getLevel());
        query.trace("Before execution", 1);
        Chain<Searcher> forkingChain = new Chain<>(new Tracer("forker"), 
                                                   new Forker(carryOverContext, parallel, 
                                                              new Tracer("branch 1") ,
                                                              new Tracer("branch 2")));
        new Execution(forkingChain, Execution.Context.createContextStub()).search(query);

        if (carryOverContext)
            assertTraceWithChildExecutionMessages(query);
        else if (parallel)
            assertTrace(query);
        else
            assertIncorrectlyNestedTrace(query);

        assertCorrectRendering(query);
    }

    // The valid and usual trace
    private void assertTraceWithChildExecutionMessages(Query query) {
        Iterator<String> trace = collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        assertEquals("  Before execution", trace.next());
        assertEquals("  (level start)", trace.next());
        assertEquals("    During forker: 0", trace.next());
        assertEquals("    (level start)", trace.next());
        assertEquals("      During branch 1: 0", trace.next());
        assertEquals("    (level end)", trace.next());
        assertEquals("    (level start)", trace.next());
        assertEquals("      During branch 2: 0", trace.next());
        assertEquals("    (level end)", trace.next());
        assertEquals("  (level end)", trace.next());
        assertEquals("(level end)", trace.next());
        assertFalse(trace.hasNext());
    }

    // With incorrect API usage and query cloning (in parallel use) we get a valid trace
    // where the message of the execution subtrees is empty rather than "child execution". This is fine.
    private void assertTrace(Query query) {
        Iterator<String> trace=collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        assertEquals("  Before execution", trace.next());
        assertEquals("  (level start)", trace.next());
        assertEquals("    During forker: 0", trace.next());
        assertEquals("    (level start)", trace.next());
        assertEquals("      During branch 1: 0", trace.next());
        assertEquals("    (level end)", trace.next());
        assertEquals("    (level start)", trace.next());
        assertEquals("      During branch 2: 0", trace.next());
        assertEquals("    (level end)", trace.next());
        assertEquals("  (level end)", trace.next());
        assertEquals("(level end)", trace.next());
        assertFalse(trace.hasNext());
    }

    // With incorrect usage and no query cloning the trace nesting becomes incorrect
    // but all the trace messages are present.
    private void assertIncorrectlyNestedTrace(Query query) {
        Iterator<String> trace=collectTrace(query).iterator();
        assertEquals("(level start)", trace.next());
        assertEquals("  No query profile is used", trace.next());
        assertEquals("  Before execution", trace.next());
        assertEquals("  (level start)", trace.next());
        assertEquals("    During forker: 0", trace.next());
        assertEquals("    (level start)", trace.next());
        assertEquals("      During branch 1: 0", trace.next());
        assertEquals("      (level start)", trace.next());
        assertEquals("        During branch 2: 0", trace.next());
        assertEquals("      (level end)", trace.next());
        assertEquals("    (level end)", trace.next());
        assertEquals("  (level end)", trace.next());
        assertEquals("(level end)", trace.next());
        assertFalse(trace.hasNext());
    }

    private void assertCorrectRendering(Query query) {
        try {
            StringWriter writer = new StringWriter();
            query.getContext(false).render(writer);
            String expected =
                    "<meta type=\"context\">\n" +
                    "\n" +
                    "  <p>No query profile is used</p>\n" +
                    "\n" +
                    "  <p>Before execution</p>\n" +
                    "\n" +
                    "  <p>\n" +
                    "    <p>During forker: 0";
            assertEquals(expected, writer.toString().substring(0, expected.length()));
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private List<String> collectTrace(Query query) {
        TraceCollector collector = new TraceCollector();
        query.getContext(false).getTrace().accept(collector);
        return collector.trace();
    }

    private static class TraceCollector extends TraceVisitor {

        private final List<String> trace = new ArrayList<>();
        private final StringBuilder indent = new StringBuilder();

        @Override
        public void entering(TraceNode node) {
            trace.add(indent + "(level start)");
            indent.append("  ");
        }

        @Override
        public void leaving(TraceNode end) {
            indent.setLength(indent.length() - 2);
            trace.add(indent + "(level end)");
        }

        @Override
        public void visit(TraceNode node) {
            if (node.isRoot()) return;
            if (node.payload() == null) return;
            trace.add(indent + node.payload().toString());
        }

        public List<String> trace() { return trace; }
    }

    private static class Tracer extends Searcher {

        private final String name;
        private final boolean traceQuery;

        private int counter = 0;

        public Tracer(String name) {
            this(name, false);
        }

        public Tracer(String name, boolean traceQuery) {
            super(new ComponentId(name));
            this.name = name;
            this.traceQuery = traceQuery;
        }

        @Override
        public Result search(Query query, Execution execution) {
            query.trace("During " + name + ": " + (counter++), traceQuery, 1);
            return execution.search(query);
        }

    }

    private static class Forker extends Searcher {

        private final List<Searcher> branches;

        /** If true, this is using the api as recommended, if false, it is not */
        private final boolean carryOverContext;

        /** If true, simulate parallel execution by cloning the query */
        private final boolean parallel;

        public Forker(boolean carryOverContext, boolean parallel, Searcher ... branches) {
            this.carryOverContext = carryOverContext;
            this.parallel = parallel;
            this.branches = Arrays.asList(branches);
        }

        @Override
        public Result search(Query query, Execution execution) {
            Result result = execution.search(query);
            for (Searcher branch : branches) {
                Query branchQuery = parallel ? query.clone() : query;
                Result branchResult =
                        ( carryOverContext ? new Execution(branch, execution.context()) : 
                                             new Execution(branch, Execution.Context.createContextStub())).search(branchQuery);
                result.hits().add(branchResult.hits());
                result.mergeWith(branchResult);
            }
            return result;
        }

    }

    private static class Backend extends Searcher {

        private final boolean fillableHits;
        
        public Backend(String name, boolean fillableHits) {
            super(new ComponentId(name));
            this.fillableHits = fillableHits;
        }

        @Override
        public Result search(Query query, Execution execution) {
            Result result = execution.search(query);
            Hit hit0 = new Hit("hit:0");
            Hit hit1 = new Hit("hit:1");
            if (fillableHits) {
                hit0.setFillable();
                hit1.setFillable();
            }
            result.hits().add(hit0);
            result.hits().add(hit1);
            return result;
        }

    }

}