View Javadoc
1   package org.wikimedia.search.extra.router;
2   
3   import java.io.IOException;
4   import java.util.Objects;
5   
6   import javax.annotation.Nullable;
7   
8   import org.elasticsearch.common.ParseField;
9   import org.elasticsearch.common.io.stream.StreamInput;
10  import org.elasticsearch.common.io.stream.StreamOutput;
11  import org.elasticsearch.common.io.stream.Writeable;
12  import org.elasticsearch.common.xcontent.ObjectParser;
13  import org.elasticsearch.common.xcontent.XContentBuilder;
14  import org.elasticsearch.common.xcontent.XContentParser;
15  import org.elasticsearch.index.query.QueryBuilder;
16  import org.elasticsearch.index.query.QueryRewriteContext;
17  import org.wikimedia.search.extra.router.DegradedRouterQueryBuilder.DegradedCondition;
18  
19  import com.google.common.annotations.VisibleForTesting;
20  
21  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22  import lombok.EqualsAndHashCode;
23  import lombok.Getter;
24  import lombok.Setter;
25  import lombok.experimental.Accessors;
26  
27  /**
28   * Builds a token_count_router query.
29   *
30   * Getter/Setter are only for testing
31   */
32  @Getter
33  @Setter
34  @Accessors(fluent = true, chain = true)
35  public class DegradedRouterQueryBuilder extends AbstractRouterQueryBuilder<DegradedCondition, DegradedRouterQueryBuilder> {
36      public static final ParseField NAME = new ParseField("degraded_router");
37      private static final ParseField TYPE = new ParseField("type");
38      private static final ParseField BUCKET = new ParseField("bucket");
39      private static final ParseField PERCENTILE = new ParseField("percentile");
40  
41      private static final ObjectParser<DegradedRouterQueryBuilder, Void> PARSER;
42      private static final ObjectParser<DegradedConditionParserState, Void> COND_PARSER;
43  
44      static {
45          COND_PARSER = new ObjectParser<>("condition", DegradedConditionParserState::new);
46          COND_PARSER.declareString((cps, value) -> cps.type(DegradedConditionType.valueOf(value)), TYPE);
47          COND_PARSER.declareString(DegradedConditionParserState::bucket, BUCKET);
48          COND_PARSER.declareDouble(DegradedConditionParserState::percentile, PERCENTILE);
49          declareConditionFields(COND_PARSER);
50  
51          PARSER = new ObjectParser<>(NAME.getPreferredName(), DegradedRouterQueryBuilder::new);
52          declareStandardFields(PARSER);
53          declareRouterFields(PARSER, (p, pc) -> parseCondition(COND_PARSER, p));
54      }
55  
56      // This intentionally is not considered in doEquals or doHashCode, as
57      // it's not part of the definition of the qb but a helper service.
58      @Nullable private SystemLoad systemLoad;
59  
60      DegradedRouterQueryBuilder() {
61          super();
62      }
63  
64      public DegradedRouterQueryBuilder(StreamInput in, SystemLoad systemLoad) throws IOException {
65          super(in, DegradedCondition::new);
66          this.systemLoad = systemLoad;
67      }
68  
69      @Override
70      public String getWriteableName() {
71          return NAME.getPreferredName();
72      }
73  
74      public static DegradedRouterQueryBuilder fromXContent(
75              XContentParser parser, SystemLoad systemLoad
76      ) throws IOException {
77          DegradedRouterQueryBuilder builder = AbstractRouterQueryBuilder.fromXContent(PARSER, parser);
78          builder.systemLoad = systemLoad;
79          return builder;
80      }
81  
82      @Override
83      public QueryBuilder doRewrite(QueryRewriteContext context) throws IOException {
84          // The nowInMillis call tells certain implementations of QueryRewriteContext
85          // that the results of this rewrite are not cacheable.
86          context.nowInMillis();
87          if (context.convertToShardContext() == null) {
88              // We want to rewrite on the shard not the coordinating node.
89              return this;
90          }
91          return super.doRewrite(condition -> condition.test(systemLoad));
92      }
93  
94      @EqualsAndHashCode(callSuper = true)
95      @Getter
96      static class DegradedCondition extends AbstractRouterQueryBuilder.Condition {
97          private final String bucket;
98          private final Double percentile;
99          private final DegradedConditionType type;
100 
101         @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
102         DegradedCondition(StreamInput in) throws IOException {
103             super(in);
104             bucket = in.readOptionalString();
105             percentile = in.readOptionalDouble();
106             type = DegradedConditionType.readFrom(in);
107         }
108 
109         DegradedCondition(ConditionDefinition definition, DegradedConditionType type, String bucket, Double percentile, int value, QueryBuilder query) {
110             super(definition, value, query);
111             this.bucket = bucket;
112             this.percentile = percentile;
113             this.type = Objects.requireNonNull(type);
114         }
115 
116         @Override
117         public void writeTo(StreamOutput out) throws IOException {
118             super.writeTo(out);
119             out.writeOptionalString(bucket);
120             out.writeOptionalDouble(percentile);
121             type.writeTo(out);
122         }
123 
124         public boolean test(SystemLoad stats) {
125             return test(type.extract(bucket, percentile, stats));
126         }
127 
128         void addXContent(XContentBuilder builder, Params params) throws IOException {
129             if (bucket != null) {
130                 builder.field(BUCKET.getPreferredName(), bucket);
131             }
132             if (percentile != null) {
133                 builder.field(PERCENTILE.getPreferredName(), percentile);
134             }
135             builder.field(TYPE.getPreferredName(), type);
136         }
137     }
138 
139     @FunctionalInterface
140     private interface LoadStatSupplier {
141         // It is certainly messy to take in all these extra pieces that only one condition type
142         // needs, but a bigger refactor was decided to be more complex than living with a little
143         // mess.
144         long extract(String bucket, Double percentile, SystemLoad stats);
145     }
146 
147     enum DegradedConditionType implements LoadStatSupplier, Writeable {
148         cpu((bucket, percentile, stats) -> stats.getCpuPercent()),
149         load((bucket, percentile, stats) -> stats.get1MinuteLoadAverage()),
150         latency((bucket, percentile, stats) -> stats.getLatency(bucket, percentile)) {
151             @Override
152             public void checkValid(@Nullable String bucket, @Nullable Double percentile) {
153                 if (bucket == null) {
154                     throw new IllegalArgumentException("Missing field [bucket] in condition");
155                 }
156                 if (percentile == null) {
157                     throw new IllegalArgumentException("Missing field [percentile] in condition");
158                 }
159             }
160         };
161 
162         private final LoadStatSupplier extractor;
163 
164         DegradedConditionType(LoadStatSupplier extractor) {
165             this.extractor = extractor;
166         }
167 
168         public long extract(String bucket, Double percentile, SystemLoad stats) {
169             return extractor.extract(bucket, percentile, stats);
170         }
171 
172         @Override
173         public void writeTo(StreamOutput out) throws IOException {
174             out.writeVInt(ordinal());
175         }
176 
177         static DegradedConditionType readFrom(StreamInput in) throws IOException {
178             int ord = in.readVInt();
179             if (ord < 0 || ord >= values().length) {
180                 throw new IOException("Unknown ConditionDefinition ordinal [" + ord + "]");
181             }
182             return values()[ord];
183         }
184 
185         void checkValid(String bucket, Double percentile) {
186             if (bucket != null) {
187                 throw new IllegalArgumentException("Extra field [bucket] in condition");
188             }
189             if (percentile != null) {
190                 throw new IllegalArgumentException("Extra field [percentile] in condition");
191             }
192         }
193 
194     }
195 
196     @Setter
197     private static final class DegradedConditionParserState extends AbstractConditionParserState<DegradedCondition> {
198         @Nullable private DegradedConditionType type;
199         @Nullable private String bucket;
200         @Nullable private Double percentile;
201 
202         DegradedCondition condition() {
203             return new DegradedCondition(definition, type, bucket, percentile, value, query);
204         }
205 
206         @Override
207         void checkValid() {
208             super.checkValid();
209             type.checkValid(bucket, percentile);
210         }
211     }
212 
213     @VisibleForTesting
214     void condition(ConditionDefinition def, DegradedConditionType type, String bucket, Double percentile, int value, QueryBuilder query) {
215         condition(new DegradedCondition(def, type, bucket, percentile, value, query));
216     }
217 }