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

grpc / grpc-java / #19713

04 Mar 2025 04:32AM UTC coverage: 88.515% (-0.02%) from 88.538%
#19713

push

github

web-flow
rls: allow maxAge to exceed 5m if staleAge is set (#11931)

34427 of 38894 relevant lines covered (88.51%)

0.89 hits per line

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

97.01
/../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
    }
69

70
    @Override
71
    protected RouteLookupRequest doBackward(RlsProtoData.RouteLookupRequest routeLookupRequest) {
72
      return
1✔
73
          RouteLookupRequest.newBuilder()
1✔
74
              .setTargetType("grpc")
1✔
75
              .putAllKeyMap(routeLookupRequest.keyMap())
1✔
76
              .build();
1✔
77
    }
78
  }
79

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

271
  private RlsProtoConverters() {}
272
}
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