• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20052

05 Nov 2025 03:32PM UTC coverage: 88.552% (+0.03%) from 88.521%
#20052

push

github

ejona86
rls: Add route lookup reason to request whether it is due to a cache miss or stale cache entry (#12442)

b/348690462

34985 of 39508 relevant lines covered (88.55%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.06
/../rls/src/main/java/io/grpc/rls/RlsProtoConverters.java
1
/*
2
 * Copyright 2020 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.rls;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static java.util.concurrent.TimeUnit.MINUTES;
22
import static java.util.concurrent.TimeUnit.SECONDS;
23

24
import com.google.common.base.Converter;
25
import com.google.common.base.Strings;
26
import com.google.common.collect.ImmutableList;
27
import com.google.common.collect.ImmutableMap;
28
import io.grpc.internal.JsonUtil;
29
import io.grpc.lookup.v1.RouteLookupRequest;
30
import io.grpc.lookup.v1.RouteLookupResponse;
31
import io.grpc.rls.RlsProtoData.ExtraKeys;
32
import io.grpc.rls.RlsProtoData.GrpcKeyBuilder;
33
import io.grpc.rls.RlsProtoData.GrpcKeyBuilder.Name;
34
import io.grpc.rls.RlsProtoData.NameMatcher;
35
import io.grpc.rls.RlsProtoData.RouteLookupConfig;
36
import java.net.URI;
37
import java.net.URISyntaxException;
38
import java.util.ArrayList;
39
import java.util.HashSet;
40
import java.util.List;
41
import java.util.Map;
42
import java.util.Set;
43
import javax.annotation.Nullable;
44

45
/**
46
 * RlsProtoConverters is a collection of {@link Converter} between RouteLookupService proto / json
47
 * messages to internal representation in {@link RlsProtoData}.
48
 */
49
final class RlsProtoConverters {
50

51
  private static final long MAX_AGE_NANOS = MINUTES.toNanos(5);
1✔
52
  private static final long MAX_CACHE_SIZE = 5 * 1024 * 1024; // 5MiB
53
  private static final long DEFAULT_LOOKUP_SERVICE_TIMEOUT = SECONDS.toNanos(10);
1✔
54
  private static final ImmutableList<String> EXTRA_KEY_NAMES =
1✔
55
      ImmutableList.of("host", "service", "method");
1✔
56

57
  /**
58
   * RouteLookupRequestConverter converts between {@link RouteLookupRequest} and {@link
59
   * RlsProtoData.RouteLookupRequest}.
60
   */
61
  static final class RouteLookupRequestConverter
1✔
62
      extends Converter<RouteLookupRequest, RlsProtoData.RouteLookupRequest> {
63

64
    @Override
65
    protected RlsProtoData.RouteLookupRequest doForward(RouteLookupRequest routeLookupRequest) {
66
      return RlsProtoData.RouteLookupRequest.create(
1✔
67
          ImmutableMap.copyOf(routeLookupRequest.getKeyMapMap()),
1✔
68
          RlsProtoData.RouteLookupRequest.Reason.valueOf(routeLookupRequest.getReason().name())
1✔
69
      );
70
    }
71

72
    @Override
73
    protected RouteLookupRequest doBackward(RlsProtoData.RouteLookupRequest routeLookupRequest) {
74
      return
1✔
75
          RouteLookupRequest.newBuilder()
1✔
76
              .setTargetType("grpc")
1✔
77
              .setReason(RouteLookupRequest.Reason.valueOf(routeLookupRequest.reason().name()))
1✔
78
              .putAllKeyMap(routeLookupRequest.keyMap())
1✔
79
              .build();
1✔
80
    }
81
  }
82

83
  /**
84
   * RouteLookupResponseConverter converts between {@link RouteLookupResponse} and {@link
85
   * RlsProtoData.RouteLookupResponse}.
86
   */
87
  static final class RouteLookupResponseConverter
1✔
88
      extends Converter<RouteLookupResponse, RlsProtoData.RouteLookupResponse> {
89

90
    @Override
91
    protected RlsProtoData.RouteLookupResponse doForward(RouteLookupResponse routeLookupResponse) {
92
      return
1✔
93
          RlsProtoData.RouteLookupResponse.create(
1✔
94
              ImmutableList.copyOf(routeLookupResponse.getTargetsList()),
1✔
95
              routeLookupResponse.getHeaderData());
1✔
96
    }
97

98
    @Override
99
    protected RouteLookupResponse doBackward(RlsProtoData.RouteLookupResponse routeLookupResponse) {
100
      return RouteLookupResponse.newBuilder()
1✔
101
          .addAllTargets(routeLookupResponse.targets())
1✔
102
          .setHeaderData(routeLookupResponse.getHeaderData())
1✔
103
          .build();
1✔
104
    }
105
  }
106

107
  /**
108
   * RouteLookupConfigConverter converts between json map to {@link RouteLookupConfig}.
109
   */
110
  static final class RouteLookupConfigConverter
1✔
111
      extends Converter<Map<String, ?>, RouteLookupConfig> {
112

113
    @Override
114
    protected RouteLookupConfig doForward(Map<String, ?> json) {
115
      ImmutableList<GrpcKeyBuilder> grpcKeybuilders =
1✔
116
          GrpcKeyBuilderConverter.covertAll(
1✔
117
              checkNotNull(JsonUtil.getListOfObjects(json, "grpcKeybuilders"), "grpcKeybuilders"));
1✔
118

119
      // Validate grpc_keybuilders
120
      checkArgument(!grpcKeybuilders.isEmpty(), "must have at least one GrpcKeyBuilder");
1✔
121
      Set<Name> names = new HashSet<>();
1✔
122
      for (GrpcKeyBuilder keyBuilder : grpcKeybuilders) {
1✔
123
        for (Name name : keyBuilder.names()) {
1✔
124
          checkArgument(names.add(name), "duplicate names in grpc_keybuilders: " + name);
1✔
125
        }
1✔
126

127
        Set<String> keys = new HashSet<>();
1✔
128
        for (NameMatcher header : keyBuilder.headers()) {
1✔
129
          checkKeys(keys, header.key(), "header");
1✔
130
        }
1✔
131
        for (String key : keyBuilder.constantKeys().keySet()) {
1✔
132
          checkKeys(keys, key, "constant");
1✔
133
        }
1✔
134
        String extraKeyStr = keyToString(keyBuilder.extraKeys());
1✔
135
        checkArgument(keys.add(extraKeyStr),
1✔
136
            "duplicate extra key in grpc_keybuilders: " + extraKeyStr);
137
      }
1✔
138

139
      // Validate lookup_service
140
      String lookupService = JsonUtil.getString(json, "lookupService");
1✔
141
      checkArgument(!Strings.isNullOrEmpty(lookupService), "lookupService must not be empty");
1✔
142
      try {
143
        URI unused = new URI(lookupService);
1✔
144
      } catch (URISyntaxException e) {
×
145
        throw new IllegalArgumentException(
×
146
            "The lookupService field is not valid URI: " + lookupService, e);
147
      }
1✔
148
      long timeout = orDefault(
1✔
149
          JsonUtil.getStringAsDuration(json, "lookupServiceTimeout"),
1✔
150
          DEFAULT_LOOKUP_SERVICE_TIMEOUT);
1✔
151
      checkArgument(timeout > 0, "lookupServiceTimeout should be positive");
1✔
152
      Long maxAge = JsonUtil.getStringAsDuration(json, "maxAge");
1✔
153
      Long staleAge = JsonUtil.getStringAsDuration(json, "staleAge");
1✔
154
      if (maxAge == null) {
1✔
155
        checkArgument(staleAge == null, "to specify staleAge, must have maxAge");
1✔
156
        maxAge = MAX_AGE_NANOS;
1✔
157
      }
158
      // If staleAge is not set, clamp maxAge to <= 5.
159
      if (staleAge == null && maxAge > MAX_AGE_NANOS) {
1✔
160
        maxAge = MAX_AGE_NANOS;
×
161
      }
162
      // Clamp staleAge to <= 5
163
      if (staleAge == null || staleAge > MAX_AGE_NANOS) {
1✔
164
        staleAge = MAX_AGE_NANOS;
1✔
165
      }
166
      // Ignore staleAge if greater than maxAge.
167
      staleAge = Math.min(staleAge, maxAge);
1✔
168
      long cacheSize = orDefault(JsonUtil.getNumberAsLong(json, "cacheSizeBytes"), MAX_CACHE_SIZE);
1✔
169
      checkArgument(cacheSize > 0, "cacheSize must be positive");
1✔
170
      cacheSize = Math.min(cacheSize, MAX_CACHE_SIZE);
1✔
171
      String defaultTarget = Strings.emptyToNull(JsonUtil.getString(json, "defaultTarget"));
1✔
172
      return RouteLookupConfig.builder()
1✔
173
          .grpcKeybuilders(grpcKeybuilders)
1✔
174
          .lookupService(lookupService)
1✔
175
          .lookupServiceTimeoutInNanos(timeout)
1✔
176
          .maxAgeInNanos(maxAge)
1✔
177
          .staleAgeInNanos(staleAge)
1✔
178
          .cacheSizeBytes(cacheSize)
1✔
179
          .defaultTarget(defaultTarget)
1✔
180
          .build();
1✔
181
    }
182

183
    private static String keyToString(ExtraKeys extraKeys) {
184
      return String.format("host: %s, service: %s, method: %s",
1✔
185
          extraKeys.host(), extraKeys.service(), extraKeys.method());
1✔
186
    }
187

188
    private static <T> T orDefault(@Nullable T value, T defaultValue) {
189
      if (value == null) {
1✔
190
        return checkNotNull(defaultValue, "defaultValue");
1✔
191
      }
192
      return value;
1✔
193
    }
194

195
    @Override
196
    protected Map<String, Object> doBackward(RouteLookupConfig routeLookupConfig) {
197
      throw new UnsupportedOperationException();
×
198
    }
199
  }
200

201
  private static void checkKeys(Set<String> keys, String key, String keyType) {
202
    checkArgument(key != null, "unset " + keyType + "  key");
1✔
203
    checkArgument(!key.isEmpty(), "Empty string for " + keyType + " key");
1✔
204
    checkArgument(keys.add(key), "duplicate " + keyType + " key in grpc_keybuilders: " + key);
1✔
205
  }
1✔
206

207
  private static final class GrpcKeyBuilderConverter {
208
    public static ImmutableList<GrpcKeyBuilder> covertAll(List<Map<String, ?>> keyBuilders) {
209
      ImmutableList.Builder<GrpcKeyBuilder> keyBuilderList = ImmutableList.builder();
1✔
210
      for (Map<String, ?> keyBuilder : keyBuilders) {
1✔
211
        keyBuilderList.add(convert(keyBuilder));
1✔
212
      }
1✔
213
      return keyBuilderList.build();
1✔
214
    }
215

216
    @SuppressWarnings("unchecked")
217
    static GrpcKeyBuilder convert(Map<String, ?> keyBuilder) {
218
      List<?> rawRawNames = JsonUtil.getList(keyBuilder, "names");
1✔
219
      checkArgument(
1✔
220
          rawRawNames != null && !rawRawNames.isEmpty(),
1✔
221
          "each keyBuilder must have at least one name");
222
      List<Map<String, ?>> rawNames = JsonUtil.checkObjectList(rawRawNames);
1✔
223
      ImmutableList.Builder<Name> namesBuilder = ImmutableList.builder();
1✔
224
      for (Map<String, ?> rawName : rawNames) {
1✔
225
        String serviceName = JsonUtil.getString(rawName, "service");
1✔
226
        checkArgument(!Strings.isNullOrEmpty(serviceName), "service must not be empty or null");
1✔
227
        namesBuilder.add(Name.create(serviceName, JsonUtil.getString(rawName, "method")));
1✔
228
      }
1✔
229
      List<?> rawRawHeaders = JsonUtil.getList(keyBuilder, "headers");
1✔
230
      List<Map<String, ?>> rawHeaders =
231
          rawRawHeaders == null
1✔
232
              ? new ArrayList<Map<String, ?>>() : JsonUtil.checkObjectList(rawRawHeaders);
1✔
233
      ImmutableList.Builder<NameMatcher> nameMatchersBuilder = ImmutableList.builder();
1✔
234
      for (Map<String, ?> rawHeader : rawHeaders) {
1✔
235
        Boolean requiredMatch = JsonUtil.getBoolean(rawHeader, "requiredMatch");
1✔
236
        checkArgument(
1✔
237
            requiredMatch == null || !requiredMatch,
1✔
238
            "requiredMatch shouldn't be specified for gRPC");
239
        NameMatcher matcher = NameMatcher.create(
1✔
240
            JsonUtil.getString(rawHeader, "key"),
1✔
241
            ImmutableList.copyOf((List<String>) rawHeader.get("names")));
1✔
242
        nameMatchersBuilder.add(matcher);
1✔
243
      }
1✔
244
      ExtraKeys extraKeys = ExtraKeys.DEFAULT;
1✔
245
      Map<String, String> rawExtraKeys =
1✔
246
          (Map<String, String>) JsonUtil.getObject(keyBuilder,  "extraKeys");
1✔
247
      if (rawExtraKeys != null) {
1✔
248
        extraKeys = ExtraKeys.create(
1✔
249
            rawExtraKeys.get("host"), rawExtraKeys.get("service"), rawExtraKeys.get("method"));
1✔
250
      }
251
      Map<String, String> constantKeys =
1✔
252
          (Map<String, String>) JsonUtil.getObject(keyBuilder,  "constantKeys");
1✔
253
      if (constantKeys == null) {
1✔
254
        constantKeys = ImmutableMap.of();
1✔
255
      }
256
      ImmutableList<NameMatcher> nameMatchers = nameMatchersBuilder.build();
1✔
257
      checkUniqueKey(nameMatchers, constantKeys.keySet());
1✔
258
      return GrpcKeyBuilder.create(
1✔
259
          namesBuilder.build(), nameMatchers, extraKeys, ImmutableMap.copyOf(constantKeys));
1✔
260
    }
261
  }
262

263
  private static void checkUniqueKey(List<NameMatcher> nameMatchers, Set<String> constantKeys) {
264
    Set<String> keys = new HashSet<>(constantKeys);
1✔
265
    keys.addAll(EXTRA_KEY_NAMES);
1✔
266
    for (NameMatcher nameMatcher :  nameMatchers) {
1✔
267
      keys.add(nameMatcher.key());
1✔
268
    }
1✔
269
    if (keys.size() != nameMatchers.size() + constantKeys.size() + EXTRA_KEY_NAMES.size()) {
1✔
270
      throw new IllegalArgumentException("keys in KeyBuilder must be unique");
1✔
271
    }
272
  }
1✔
273

274
  private RlsProtoConverters() {}
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc