From 12e58b47a83b697d8531c5963302529285261057 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 7 Feb 2024 08:32:52 -0800 Subject: [PATCH 01/47] RlqsFilter WIP --- xds/src/main/java/io/grpc/xds/Filter.java | 2 + .../main/java/io/grpc/xds/FilterRegistry.java | 3 +- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 118 ++++++++++++++++++ .../java/io/grpc/xds/RlqsFilterConfig.java | 37 ++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 xds/src/main/java/io/grpc/xds/RlqsFilter.java create mode 100644 xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 4b2767687f3..d4fbbe2a787 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -70,6 +70,8 @@ ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig); } + // shutdown/close + /** Filter config with instance name. */ final class NamedFilterConfig { // filter instance name diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index 7f1fe82c6c3..ae932746761 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -37,7 +37,8 @@ static synchronized FilterRegistry getDefaultRegistry() { instance = newRegistry().register( FaultFilter.INSTANCE, RouterFilter.INSTANCE, - RbacFilter.INSTANCE); + RbacFilter.INSTANCE, + RlqsFilter.INSTANCE); } return instance; } diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java new file mode 100644 index 00000000000..f38ecf2b383 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.XdsResourceType.ResourceInvalidException; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig; +import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; +import io.grpc.ServerInterceptor; +import io.grpc.xds.Filter.ServerInterceptorBuilder; +import javax.annotation.Nullable; + +/** RBAC Http filter implementation. */ +final class RlqsFilter implements Filter, ServerInterceptorBuilder { + // private static final Logger logger = Logger.getLogger(RlqsFilter.class.getName()); + + static final RlqsFilter INSTANCE = new RlqsFilter(); + + static final String TYPE_URL = "type.googleapis.com/" + + "envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig"; + static final String TYPE_URL_OVERRIDE_CONFIG = "type.googleapis.com/" + + "envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride"; + + @Override + public String[] typeUrls() { + return new String[] { TYPE_URL, TYPE_URL_OVERRIDE_CONFIG }; + } + + @Override + public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + try { + RlqsFilterConfig rlqsFilterConfig = + parseRlqsFilter(unpackConfigMessage(rawProtoMessage, RateLimitQuotaFilterConfig.class)); + return ConfigOrError.fromConfig(rlqsFilterConfig); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Can't unpack RateLimitQuotaFilterConfig proto: " + e); + } catch (ResourceInvalidException e) { + return ConfigOrError.fromError(e.getMessage()); + } + } + + @Override + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + try { + RateLimitQuotaOverride rlqsFilterOverrideProto = + unpackConfigMessage(rawProtoMessage, RateLimitQuotaOverride.class); + return ConfigOrError.fromConfig(parseRlqsFilterOverride(rlqsFilterOverrideProto)); + } catch (InvalidProtocolBufferException e) { + return ConfigOrError.fromError("Can't unpack RateLimitQuotaFilterConfig proto: " + e); + } catch (ResourceInvalidException e) { + return ConfigOrError.fromError(e.getMessage()); + } + } + + @Nullable + @Override + public ServerInterceptor buildServerInterceptor( + FilterConfig config, @Nullable FilterConfig overrideConfig) { + checkNotNull(config, "config"); + if (overrideConfig != null) { + config = overrideConfig; + } + // todo + config.typeUrl(); // used + return null; + } + + // @VisibleForTesting + static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilterProtoOverride) + throws ResourceInvalidException { + String domain; + if (!rlqsFilterProtoOverride.getDomain().isEmpty()) { + domain = rlqsFilterProtoOverride.getDomain(); + } else { + domain = "YOYOOYOYO"; + } + // todo: parse the reset + return RlqsFilterConfig.create(domain); + } + + // @VisibleForTesting + static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) + throws ResourceInvalidException { + if (rlqsFilterProto.getDomain().isEmpty()) { + throw new ResourceInvalidException("RateLimitQuotaFilterConfig domain is required"); + } + // TODO(sergiitk): parse rlqs_server, bucket_matchers. + return RlqsFilterConfig.create(rlqsFilterProto.getDomain()); + } + + private static T unpackConfigMessage( + Message message, Class clazz) throws InvalidProtocolBufferException { + if (!(message instanceof Any)) { + throw new InvalidProtocolBufferException("Invalid config type: " + message.getClass()); + } + return ((Any) message).unpack(clazz); + } +} + diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java new file mode 100644 index 00000000000..72320c22a2c --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.auto.value.AutoValue; +import io.grpc.xds.Filter.FilterConfig; + +/** Parsed RateLimitQuotaFilterConfig. */ +@AutoValue +abstract class RlqsFilterConfig implements FilterConfig { + @Override + public final String typeUrl() { + return RlqsFilter.TYPE_URL; + } + + abstract String domain(); + + // TODO(sergiitk): add rlqs_server, bucket_matchers. + + static RlqsFilterConfig create(String domain) { + return new AutoValue_RlqsFilterConfig(domain); + } +} From a4de284a5bb2bbde364ba1e81a69b2c998ce1ea4 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 7 Feb 2024 08:32:52 -0800 Subject: [PATCH 02/47] Basic GrpcService type --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 47 ++++++++++--------- .../java/io/grpc/xds/RlqsFilterConfig.java | 11 +++-- .../xds/internal/datatype/GrpcService.java | 19 ++++++++ 3 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index f38ecf2b383..2f11960616e 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.xds.XdsResourceType.ResourceInvalidException; +import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -27,6 +28,7 @@ import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; import io.grpc.ServerInterceptor; import io.grpc.xds.Filter.ServerInterceptorBuilder; +import io.grpc.xds.internal.datatype.GrpcService; import javax.annotation.Nullable; /** RBAC Http filter implementation. */ @@ -71,40 +73,43 @@ public ConfigOrError parseFilterConfigOverride(Message rawProt } } - @Nullable - @Override - public ServerInterceptor buildServerInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig) { - checkNotNull(config, "config"); - if (overrideConfig != null) { - config = overrideConfig; + @VisibleForTesting + static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) + throws ResourceInvalidException { + if (rlqsFilterProto.getDomain().isEmpty()) { + throw new ResourceInvalidException("RateLimitQuotaFilterConfig domain is required"); } - // todo - config.typeUrl(); // used - return null; + + GrpcService rlqsService = GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer()); + + // TODO(sergiitk): parse rlqs_server, bucket_matchers. + return RlqsFilterConfig.create(rlqsFilterProto.getDomain(), rlqsService); } - // @VisibleForTesting + @VisibleForTesting static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilterProtoOverride) throws ResourceInvalidException { String domain; if (!rlqsFilterProtoOverride.getDomain().isEmpty()) { domain = rlqsFilterProtoOverride.getDomain(); } else { - domain = "YOYOOYOYO"; + domain = "MAGIC_USE_FILTER_CONFIG"; } - // todo: parse the reset - return RlqsFilterConfig.create(domain); + // todo: parse the rest + return RlqsFilterConfig.create(domain, null); } - // @VisibleForTesting - static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) - throws ResourceInvalidException { - if (rlqsFilterProto.getDomain().isEmpty()) { - throw new ResourceInvalidException("RateLimitQuotaFilterConfig domain is required"); + @Nullable + @Override + public ServerInterceptor buildServerInterceptor( + FilterConfig config, @Nullable FilterConfig overrideConfig) { + checkNotNull(config, "config"); + if (overrideConfig != null) { + config = overrideConfig; } - // TODO(sergiitk): parse rlqs_server, bucket_matchers. - return RlqsFilterConfig.create(rlqsFilterProto.getDomain()); + // todo + config.typeUrl(); // used + return null; } private static T unpackConfigMessage( diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index 72320c22a2c..ce59001a4f7 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -18,20 +18,23 @@ import com.google.auto.value.AutoValue; import io.grpc.xds.Filter.FilterConfig; +import io.grpc.xds.internal.datatype.GrpcService; /** Parsed RateLimitQuotaFilterConfig. */ @AutoValue abstract class RlqsFilterConfig implements FilterConfig { + @Override public final String typeUrl() { return RlqsFilter.TYPE_URL; } abstract String domain(); + abstract GrpcService rlqsService(); - // TODO(sergiitk): add rlqs_server, bucket_matchers. - - static RlqsFilterConfig create(String domain) { - return new AutoValue_RlqsFilterConfig(domain); + public static RlqsFilterConfig create(String domain, GrpcService rlqsService) { + return new AutoValue_RlqsFilterConfig(domain, rlqsService); } + + // TODO(sergiitk): add rlqs_server, bucket_matchers. } diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java new file mode 100644 index 00000000000..cf23e3103ab --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java @@ -0,0 +1,19 @@ +package io.grpc.xds.internal.datatype; + +import com.google.auto.value.AutoValue; +import com.google.protobuf.Duration; + +@AutoValue +public abstract class GrpcService { + + abstract Duration timeout(); + + public static GrpcService fromEnvoyProto( + io.envoyproxy.envoy.config.core.v3.GrpcService grpcService) { + return GrpcService.create(grpcService.getTimeout()); + } + + public static GrpcService create(Duration timeout) { + return new AutoValue_GrpcService(timeout); + } +} From 0709108115cc81c11d39cde889b4e57270ff0324 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 7 Feb 2024 08:32:52 -0800 Subject: [PATCH 03/47] Basic GrpcService --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 43 ++++++------ .../java/io/grpc/xds/RlqsFilterConfig.java | 1 + .../io/grpc/xds/client/XdsResourceType.java | 8 +++ .../xds/internal/datatype/GrpcService.java | 66 +++++++++++++++++-- 4 files changed, 93 insertions(+), 25 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 2f11960616e..14a9209d128 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -44,14 +44,14 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { @Override public String[] typeUrls() { - return new String[] { TYPE_URL, TYPE_URL_OVERRIDE_CONFIG }; + return new String[]{TYPE_URL, TYPE_URL_OVERRIDE_CONFIG}; } @Override public ConfigOrError parseFilterConfig(Message rawProtoMessage) { try { RlqsFilterConfig rlqsFilterConfig = - parseRlqsFilter(unpackConfigMessage(rawProtoMessage, RateLimitQuotaFilterConfig.class)); + parseRlqsFilter(unpackAny(rawProtoMessage, RateLimitQuotaFilterConfig.class)); return ConfigOrError.fromConfig(rlqsFilterConfig); } catch (InvalidProtocolBufferException e) { return ConfigOrError.fromError("Can't unpack RateLimitQuotaFilterConfig proto: " + e); @@ -63,16 +63,29 @@ public ConfigOrError parseFilterConfig(Message rawProtoMessage @Override public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { try { - RateLimitQuotaOverride rlqsFilterOverrideProto = - unpackConfigMessage(rawProtoMessage, RateLimitQuotaOverride.class); - return ConfigOrError.fromConfig(parseRlqsFilterOverride(rlqsFilterOverrideProto)); + RlqsFilterConfig rlqsFilterConfig = + parseRlqsFilterOverride(unpackAny(rawProtoMessage, RateLimitQuotaOverride.class)); + return ConfigOrError.fromConfig(rlqsFilterConfig); } catch (InvalidProtocolBufferException e) { - return ConfigOrError.fromError("Can't unpack RateLimitQuotaFilterConfig proto: " + e); + return ConfigOrError.fromError("Can't unpack RateLimitQuotaOverride proto: " + e); } catch (ResourceInvalidException e) { return ConfigOrError.fromError(e.getMessage()); } } + @Nullable + @Override + public ServerInterceptor buildServerInterceptor( + FilterConfig config, @Nullable FilterConfig overrideConfig) { + checkNotNull(config, "config"); + if (overrideConfig != null) { + config = overrideConfig; + } + // todo + config.typeUrl(); // used + return null; + } + @VisibleForTesting static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) throws ResourceInvalidException { @@ -99,23 +112,11 @@ static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilte return RlqsFilterConfig.create(domain, null); } - @Nullable - @Override - public ServerInterceptor buildServerInterceptor( - FilterConfig config, @Nullable FilterConfig overrideConfig) { - checkNotNull(config, "config"); - if (overrideConfig != null) { - config = overrideConfig; - } - // todo - config.typeUrl(); // used - return null; - } - - private static T unpackConfigMessage( + private static T unpackAny( Message message, Class clazz) throws InvalidProtocolBufferException { if (!(message instanceof Any)) { - throw new InvalidProtocolBufferException("Invalid config type: " + message.getClass()); + throw new InvalidProtocolBufferException( + "Invalid config type: " + message.getClass().getCanonicalName()); } return ((Any) message).unpack(clazz); } diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index ce59001a4f7..592173f6aad 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -30,6 +30,7 @@ public final String typeUrl() { } abstract String domain(); + abstract GrpcService rlqsService(); public static RlqsFilterConfig create(String domain, GrpcService rlqsService) { diff --git a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java index 8c3d31604e4..0e810d696c1 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsResourceType.java @@ -132,6 +132,14 @@ public ResourceInvalidException(String message) { public ResourceInvalidException(String message, Throwable cause) { super(cause != null ? message + ": " + cause.getMessage() : message, cause, false, false); } + + public static ResourceInvalidException ofResource(String resourceName, String reason) { + return new ResourceInvalidException("Error parsing " + resourceName + ": " + reason); + } + + public static ResourceInvalidException ofResource(Message proto, String reason) { + return ResourceInvalidException.ofResource(proto.getClass().getCanonicalName(), reason); + } } ValidatedResourceUpdate parse(Args args, List resources) { diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java index cf23e3103ab..2f34f389da1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java @@ -1,19 +1,77 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.grpc.xds.internal.datatype; +import static io.grpc.xds.XdsResourceType.ResourceInvalidException; + import com.google.auto.value.AutoValue; import com.google.protobuf.Duration; +import javax.annotation.Nullable; @AutoValue public abstract class GrpcService { + abstract String targetUri(); + + // TODO(sergiitk): do we need this? + // abstract String statPrefix(); + // TODO(sergiitk): channelCredentials + // TODO(sergiitk): callCredentials + // TODO(sergiitk): channelArgs + + /** Optional timeout duration for the gRPC request to the service. */ + @Nullable abstract Duration timeout(); public static GrpcService fromEnvoyProto( - io.envoyproxy.envoy.config.core.v3.GrpcService grpcService) { - return GrpcService.create(grpcService.getTimeout()); + io.envoyproxy.envoy.config.core.v3.GrpcService grpcServiceProto) + throws ResourceInvalidException { + if (grpcServiceProto.getTargetSpecifierCase() + != io.envoyproxy.envoy.config.core.v3.GrpcService.TargetSpecifierCase.GOOGLE_GRPC) { + throw ResourceInvalidException.ofResource(grpcServiceProto, + "Only GoogleGrpc targets supported, got " + grpcServiceProto.getTargetSpecifierCase()); + } + Builder builder = GrpcService.builder(); + if (grpcServiceProto.hasTimeout()) { + builder.timeout(grpcServiceProto.getTimeout()); + } + // GoogleGrpc fields flattened. + io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc googleGrpcProto = + grpcServiceProto.getGoogleGrpc(); + builder.targetUri(googleGrpcProto.getTargetUri()); + + // TODO(sergiitk): channelCredentials + // TODO(sergiitk): callCredentials + // TODO(sergiitk): channelArgs + // TODO(sergiitk): statPrefix - (maybe) + + return builder.build(); + } + + public static Builder builder() { + return new AutoValue_GrpcService.Builder(); } - public static GrpcService create(Duration timeout) { - return new AutoValue_GrpcService(timeout); + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder targetUri(String targetUri); + + public abstract Builder timeout(Duration timeout); + + public abstract GrpcService build(); } } From 63e2fb1cae7f9f2b04bce021a2e386951c7d049c Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 7 Feb 2024 08:32:52 -0800 Subject: [PATCH 04/47] Basic interceptor --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 88 +++++++++++++++---- .../java/io/grpc/xds/RlqsFilterConfig.java | 17 +++- 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 14a9209d128..5ea7642945d 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -26,6 +26,10 @@ import com.google.protobuf.Message; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; @@ -73,43 +77,93 @@ public ConfigOrError parseFilterConfigOverride(Message rawProt } } - @Nullable @Override public ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig) { - checkNotNull(config, "config"); + RlqsFilterConfig rlqsFilterConfig = (RlqsFilterConfig) checkNotNull(config, "config"); + + // Per-route and per-host configuration overrides. if (overrideConfig != null) { - config = overrideConfig; + RlqsFilterConfig rlqsFilterOverride = (RlqsFilterConfig) overrideConfig; + // All fields are inherited from the main config, unless overriden. + RlqsFilterConfig.Builder overrideBuilder = rlqsFilterConfig.toBuilder(); + if (!rlqsFilterOverride.domain().isEmpty()) { + overrideBuilder.domain(rlqsFilterOverride.domain()); + } + // Override bucket matchers if not null. + rlqsFilterConfig = overrideBuilder.build(); } - // todo - config.typeUrl(); // used - return null; + + return generateRlqsInterceptor(rlqsFilterConfig); + } + + private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { + checkNotNull(config, "config"); + checkNotNull(config.rlqsService(), "config.rlqsService"); + // final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config); + return new ServerInterceptor() { + @Override + public Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + // TODO(sergiitk): Why `final call` in RbacFilter? + + // Notes: + // map domain() -> object + // shared resource holder, acquire every rpc + // Store RLQS Client or channel in the config as a reference - FilterConfig config ref + // when parse. + // - atomic maybe + // - allocate channel on demand / ref counting + // - and interface to notify service interceptor on shutdown + // - destroy channel when ref count 0 + // potentially many RLQS Clients sharing a channel to grpc RLQS service - + // TODO(sergiitk): look up how cache is looked up + // now we create filters every RPC. will be change in RBAC. + // we need to avoid recreating filter when config doesn't change + // m: trigger close() after we create new instances + // RBAC filter recreate? - has to be fixed for RBAC + // AI: follow up with Eric on how cache is shared, this changes if we need to cache + // interceptor + + // Example: + // AuthDecision authResult = authEngine.evaluate(headers, call); + // if (logger.isLoggable(Level.FINE)) { + // logger.log(Level.FINE, + // "Authorization result for serverCall {0}: {1}, matching policy: {2}.", + // new Object[]{call, authResult.decision(), authResult.matchingPolicyName()}); + // } + // if (GrpcAuthorizationEngine.Action.DENY.equals(authResult.decision())) { + // Status status = Status.PERMISSION_DENIED.withDescription("Access Denied"); + // call.close(status, new Metadata()); + // return new ServerCall.Listener(){}; + // } + return next.startCall(call, headers); + } + }; } @VisibleForTesting static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) throws ResourceInvalidException { + RlqsFilterConfig.Builder builder = RlqsFilterConfig.builder(); if (rlqsFilterProto.getDomain().isEmpty()) { throw new ResourceInvalidException("RateLimitQuotaFilterConfig domain is required"); } + builder.domain(rlqsFilterProto.getDomain()) + .rlqsService(GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer())); - GrpcService rlqsService = GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer()); + // TODO(sergiitk): bucket_matchers. - // TODO(sergiitk): parse rlqs_server, bucket_matchers. - return RlqsFilterConfig.create(rlqsFilterProto.getDomain(), rlqsService); + return builder.build(); } @VisibleForTesting static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilterProtoOverride) throws ResourceInvalidException { - String domain; - if (!rlqsFilterProtoOverride.getDomain().isEmpty()) { - domain = rlqsFilterProtoOverride.getDomain(); - } else { - domain = "MAGIC_USE_FILTER_CONFIG"; - } - // todo: parse the rest - return RlqsFilterConfig.create(domain, null); + RlqsFilterConfig.Builder builder = RlqsFilterConfig.builder(); + // TODO(sergiitk): bucket_matchers. + + return builder.domain(rlqsFilterProtoOverride.getDomain()).build(); } private static T unpackAny( diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index 592173f6aad..809d179abf0 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -19,6 +19,7 @@ import com.google.auto.value.AutoValue; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.internal.datatype.GrpcService; +import javax.annotation.Nullable; /** Parsed RateLimitQuotaFilterConfig. */ @AutoValue @@ -31,10 +32,22 @@ public final String typeUrl() { abstract String domain(); + @Nullable abstract GrpcService rlqsService(); - public static RlqsFilterConfig create(String domain, GrpcService rlqsService) { - return new AutoValue_RlqsFilterConfig(domain, rlqsService); + public static Builder builder() { + return new AutoValue_RlqsFilterConfig.Builder(); + } + + abstract Builder toBuilder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder domain(String domain); + + abstract Builder rlqsService(GrpcService rlqsService); + + abstract RlqsFilterConfig build(); } // TODO(sergiitk): add rlqs_server, bucket_matchers. From 1d225e2a532b9b0730c95bfc45b20240edcd1d25 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 12 Feb 2024 11:03:26 -0800 Subject: [PATCH 05/47] Notes from the sync with Eric --- xds/src/main/java/io/grpc/xds/Filter.java | 1 + xds/src/main/java/io/grpc/xds/RlqsFilter.java | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index d4fbbe2a787..8cab89d4830 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -70,6 +70,7 @@ ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig); } + // TODO(sergiitk): important to cover and discuss in the design. // shutdown/close /** Filter config with instance name. */ diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 5ea7642945d..197df7d149c 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -80,6 +80,8 @@ public ConfigOrError parseFilterConfigOverride(Message rawProt @Override public ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig) { + // called when we get an xds update - when the LRS or RLS changes. + // TODO(sergiitk): this needs to be confirmed. RlqsFilterConfig rlqsFilterConfig = (RlqsFilterConfig) checkNotNull(config, "config"); // Per-route and per-host configuration overrides. @@ -105,10 +107,8 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { @Override public Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - // TODO(sergiitk): Why `final call` in RbacFilter? - // Notes: - // map domain() -> object + // map domain() -> an incarnation of bucket matchers, f.e. new RlqsEngine(domain, matchers). // shared resource holder, acquire every rpc // Store RLQS Client or channel in the config as a reference - FilterConfig config ref // when parse. @@ -124,6 +124,7 @@ public Listener interceptCall( // RBAC filter recreate? - has to be fixed for RBAC // AI: follow up with Eric on how cache is shared, this changes if we need to cache // interceptor + // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. // Example: // AuthDecision authResult = authEngine.evaluate(headers, call); From 8f4f02c29ad9790f31091d48bd568d0bf73055ea Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 19 Mar 2024 10:53:37 -0700 Subject: [PATCH 06/47] post-rebase fix --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 2 +- .../main/java/io/grpc/xds/internal/datatype/GrpcService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 197df7d149c..7fa32b0b842 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.XdsResourceType.ResourceInvalidException; +import static io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.Any; diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java index 2f34f389da1..a8cef072e82 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java @@ -16,7 +16,7 @@ package io.grpc.xds.internal.datatype; -import static io.grpc.xds.XdsResourceType.ResourceInvalidException; +import static io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import com.google.auto.value.AutoValue; import com.google.protobuf.Duration; From 962a4cc35052b3801596c96863fbe9e331387a33 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 26 Mar 2024 08:59:40 -0700 Subject: [PATCH 07/47] RlqsClientPool, RlqsClient, working on shutdown --- xds/src/main/java/io/grpc/xds/Filter.java | 18 ++- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 39 ++++++- .../java/io/grpc/xds/XdsServerWrapper.java | 8 +- .../xds/internal/datatype/GrpcService.java | 4 +- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 36 ++++++ .../xds/internal/rlqs/RlqsClientPool.java | 109 ++++++++++++++++++ .../io/grpc/xds/XdsServerWrapperTest.java | 19 ++- 7 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 8cab89d4830..056a48e43bb 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -50,6 +50,12 @@ interface Filter { */ ConfigOrError parseFilterConfigOverride(Message rawProtoMessage); + default void shutdown() { + // Implement as needed. + // TODO(sergiitk): important to cover and discuss in the design. + // TODO(sergiitk): should it be in ServerInterceptorBuilder? + } + /** Represents an opaque data structure holding configuration for a filter. */ interface FilterConfig { String typeUrl(); @@ -68,10 +74,16 @@ interface ServerInterceptorBuilder { @Nullable ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig); - } - // TODO(sergiitk): important to cover and discuss in the design. - // shutdown/close + @Nullable + default ServerInterceptor buildServerInterceptor( + FilterConfig config, + @Nullable FilterConfig overrideConfig, + ScheduledExecutorService scheduler) { + return buildServerInterceptor(config, overrideConfig); + } + + } /** Filter config with instance name. */ final class NamedFilterConfig { diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 7fa32b0b842..f5f4d12e152 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -16,7 +16,6 @@ package io.grpc.xds; - import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.xds.client.XdsResourceType.ResourceInvalidException; @@ -33,6 +32,9 @@ import io.grpc.ServerInterceptor; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.internal.rlqs.RlqsClientPool; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; /** RBAC Http filter implementation. */ @@ -46,6 +48,12 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { static final String TYPE_URL_OVERRIDE_CONFIG = "type.googleapis.com/" + "envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride"; + private final AtomicReference rlqsClientPoolRef = new AtomicReference<>(); + + // RlqsFilter() { + // rlqsClientPool = new RlqsClientPool() + // } + @Override public String[] typeUrls() { return new String[]{TYPE_URL, TYPE_URL_OVERRIDE_CONFIG}; @@ -77,9 +85,18 @@ public ConfigOrError parseFilterConfigOverride(Message rawProt } } + @Nullable @Override public ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig) { + throw new UnsupportedOperationException("ScheduledExecutorService scheduler required"); + } + + @Override + public ServerInterceptor buildServerInterceptor( + FilterConfig config, + @Nullable FilterConfig overrideConfig, + ScheduledExecutorService scheduler) { // called when we get an xds update - when the LRS or RLS changes. // TODO(sergiitk): this needs to be confirmed. RlqsFilterConfig rlqsFilterConfig = (RlqsFilterConfig) checkNotNull(config, "config"); @@ -87,7 +104,7 @@ public ServerInterceptor buildServerInterceptor( // Per-route and per-host configuration overrides. if (overrideConfig != null) { RlqsFilterConfig rlqsFilterOverride = (RlqsFilterConfig) overrideConfig; - // All fields are inherited from the main config, unless overriden. + // All fields are inherited from the main config, unless overridden. RlqsFilterConfig.Builder overrideBuilder = rlqsFilterConfig.toBuilder(); if (!rlqsFilterOverride.domain().isEmpty()) { overrideBuilder.domain(rlqsFilterOverride.domain()); @@ -96,12 +113,30 @@ public ServerInterceptor buildServerInterceptor( rlqsFilterConfig = overrideBuilder.build(); } + rlqsClientPoolRef.compareAndSet(null, RlqsClientPool.newInstance(scheduler)); return generateRlqsInterceptor(rlqsFilterConfig); } + @Override + public void shutdown() { + // TODO(sergiitk): besides shutting down everything, should there be a per-route destructor? + RlqsClientPool oldClientPool = rlqsClientPoolRef.getAndUpdate(unused -> null); + if (oldClientPool != null) { + oldClientPool.shutdown(); + } + } + + @Nullable private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { checkNotNull(config, "config"); checkNotNull(config.rlqsService(), "config.rlqsService"); + RlqsClientPool rlqsClientPool = rlqsClientPoolRef.get(); + if (rlqsClientPool == null) { + // Being shut down, return no interceptor. + return null; + } + rlqsClientPool.addClient(config.rlqsService()); + // final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config); return new ServerInterceptor() { @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index bd622a71124..7dd02985f1f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -516,9 +516,11 @@ private ImmutableMap generatePerRouteInterceptors( FilterConfig filterConfig = namedFilterConfig.filterConfig; Filter filter = filterRegistry.get(filterConfig.typeUrl()); if (filter instanceof ServerInterceptorBuilder) { - ServerInterceptor interceptor = - ((ServerInterceptorBuilder) filter).buildServerInterceptor( - filterConfig, selectedOverrideConfigs.get(namedFilterConfig.name)); + ServerInterceptorBuilder interceptorBuilder = (ServerInterceptorBuilder) filter; + ServerInterceptor interceptor = interceptorBuilder.buildServerInterceptor( + filterConfig, + selectedOverrideConfigs.get(namedFilterConfig.name), + timeService); if (interceptor != null) { filterInterceptors.add(interceptor); } diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java index a8cef072e82..2336e20de60 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java @@ -24,7 +24,7 @@ @AutoValue public abstract class GrpcService { - abstract String targetUri(); + public abstract String targetUri(); // TODO(sergiitk): do we need this? // abstract String statPrefix(); @@ -35,7 +35,7 @@ public abstract class GrpcService { /** Optional timeout duration for the gRPC request to the service. */ @Nullable - abstract Duration timeout(); + public abstract Duration timeout(); public static GrpcService fromEnvoyProto( io.envoyproxy.envoy.config.core.v3.GrpcService grpcServiceProto) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java new file mode 100644 index 00000000000..b6faeecdad4 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import java.util.logging.Level; +import java.util.logging.Logger; + +final class RlqsClient { + private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); + + private final String targetUri; + + RlqsClient(String targetUri) { + this.targetUri = targetUri; + } + + + public void shutdown() { + logger.log(Level.FINER, "Shutting down RlqsClient to {0}", targetUri); + // TODO(sergiitk): impl + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java new file mode 100644 index 00000000000..baa6e5dda1b --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.Sets; +import io.grpc.SynchronizationContext; +import io.grpc.xds.internal.datatype.GrpcService; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class RlqsClientPool { + private static final Logger logger = Logger.getLogger(RlqsClientPool.class.getName()); + + // TODO(sergiitk): make a param? + private static final int DEFAULT_CLEANUP_INTERVAL_SECONDS = 10; + + // TODO(sergiitk): always in sync context? + private boolean shutdown; + private final SynchronizationContext syncContext = new SynchronizationContext((thread, error) -> { + String message = "Uncaught exception in RlqsClientPool SynchronizationContext. Panic!"; + logger.log(Level.FINE, message, error); + throw new RlqsPoolSynchronizationException(message, error); + }); + + private final ConcurrentHashMap clientPool = new ConcurrentHashMap<>(); + Set clientsToShutdown = Sets.newConcurrentHashSet(); + private final ScheduledExecutorService timeService; + private final int cleanupIntervalSeconds; + + + private RlqsClientPool(ScheduledExecutorService scheduler, int cleanupIntervalSeconds) { + this.timeService = checkNotNull(scheduler, "scheduler"); + checkArgument(cleanupIntervalSeconds >= 0, "cleanupIntervalSeconds < 0"); + this.cleanupIntervalSeconds = + cleanupIntervalSeconds > 0 ? cleanupIntervalSeconds : DEFAULT_CLEANUP_INTERVAL_SECONDS; + } + + /** Creates an instance. */ + public static RlqsClientPool newInstance(ScheduledExecutorService scheduler) { + return new RlqsClientPool(scheduler, 0); + } + + public void run() { + Runnable cleanupTask = () -> { + if (shutdown) { + return; + } + for (String targetUri : clientsToShutdown) { + clientPool.get(targetUri).shutdown(); + clientPool.remove(targetUri); + } + clientsToShutdown.clear(); + }; + syncContext.schedule(cleanupTask, cleanupIntervalSeconds, TimeUnit.SECONDS, timeService); + } + + public void shutdown() { + syncContext.execute(() -> { + shutdown = true; + logger.log(Level.FINER, "Shutting down RlqsClientPool"); + clientsToShutdown.clear(); + for (String targetUri : clientPool.keySet()) { + clientPool.get(targetUri).shutdown(); + } + clientPool.clear(); + }); + } + + public void addClient(GrpcService rlqsService) { + syncContext.execute(() -> { + RlqsClient rlqsClient = new RlqsClient(rlqsService.targetUri()); + clientPool.put(rlqsService.targetUri(), rlqsClient); + }); + } + + /** + * Throws when fail to bootstrap or initialize the XdsClient. + */ + public static final class RlqsPoolSynchronizationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public RlqsPoolSynchronizationException(String message, Throwable cause) { + super(message, cause); + } + } + + +} diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index 66ac1475d8e..b06195b9abf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -79,6 +79,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; @@ -108,19 +109,21 @@ public class XdsServerWrapperTest { private XdsServingStatusListener listener; private FilterChainSelectorManager selectorManager = new FilterChainSelectorManager(); - private FakeClock executor = new FakeClock(); + private final FakeClock executor = new FakeClock(); + private final ScheduledExecutorService timeService = executor.getScheduledExecutorService(); private FakeXdsClient xdsClient = new FakeXdsClient(); private FilterRegistry filterRegistry = FilterRegistry.getDefaultRegistry(); private XdsServerWrapper xdsServerWrapper; private ServerRoutingConfig noopConfig = ServerRoutingConfig.create( ImmutableList.of(), ImmutableMap.of()); + @Before public void setup() { when(mockBuilder.build()).thenReturn(mockServer); xdsServerWrapper = new XdsServerWrapper("0.0.0.0:1", mockBuilder, listener, selectorManager, new FakeXdsClientPoolFactory(xdsClient), - filterRegistry, executor.getScheduledExecutorService()); + filterRegistry, timeService); } @After @@ -1137,9 +1140,10 @@ public ServerCall.Listener interceptCall(ServerCallof()); @@ -1209,10 +1213,13 @@ public ServerCall.Listener interceptCall(ServerCall Date: Tue, 26 Mar 2024 10:57:02 -0700 Subject: [PATCH 08/47] another note --- xds/src/main/java/io/grpc/xds/XdsServerWrapper.java | 7 +++++++ .../java/io/grpc/xds/internal/rlqs/RlqsClientPool.java | 2 ++ 2 files changed, 9 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 7dd02985f1f..596e029dcc3 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -459,6 +459,13 @@ private void shutdown() { private void updateSelector() { Map> filterChainRouting = new HashMap<>(); + // TODO(sergiitk): is this a good place to reset interceptors? + // for (FilterChain filterChain : savedRdsRoutingConfigRef.keySet()) { + // if (!resourceName.equals(filterChain.httpConnectionManager().rdsName())) { + // continue; + // } + // + // } savedRdsRoutingConfigRef.clear(); for (FilterChain filterChain: filterChains) { filterChainRouting.put(filterChain, generateRoutingConfig(filterChain)); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index baa6e5dda1b..116f8e487f0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -58,6 +58,8 @@ private RlqsClientPool(ScheduledExecutorService scheduler, int cleanupIntervalSe /** Creates an instance. */ public static RlqsClientPool newInstance(ScheduledExecutorService scheduler) { + // TODO(sergiitk): scheduler - consider using GrpcUtil.TIMER_SERVICE. + // TODO(sergiitk): note that the scheduler has a finite lifetime. return new RlqsClientPool(scheduler, 0); } From ef9b4f7530d1bd7bb0482a9da050abbd9bd284e4 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 26 Mar 2024 11:26:36 -0700 Subject: [PATCH 09/47] categorize todos --- xds/src/main/java/io/grpc/xds/Filter.java | 4 ++-- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 14 ++++++++------ .../main/java/io/grpc/xds/RlqsFilterConfig.java | 2 +- .../main/java/io/grpc/xds/XdsServerWrapper.java | 2 +- .../grpc/xds/internal/datatype/GrpcService.java | 16 ++++++++-------- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 2 +- .../grpc/xds/internal/rlqs/RlqsClientPool.java | 7 +++---- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 056a48e43bb..9383f6df63a 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -52,8 +52,8 @@ interface Filter { default void shutdown() { // Implement as needed. - // TODO(sergiitk): important to cover and discuss in the design. - // TODO(sergiitk): should it be in ServerInterceptorBuilder? + // TODO(sergiitk): [DESIGN] important to cover and discuss in the design. + // TODO(sergiitk): [QUESTION] should it be in ServerInterceptorBuilder? } /** Represents an opaque data structure holding configuration for a filter. */ diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index f5f4d12e152..4fd7837a40f 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -97,8 +97,7 @@ public ServerInterceptor buildServerInterceptor( FilterConfig config, @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) { - // called when we get an xds update - when the LRS or RLS changes. - // TODO(sergiitk): this needs to be confirmed. + // Called when we get an xds update - when the LRS or RLS changes. RlqsFilterConfig rlqsFilterConfig = (RlqsFilterConfig) checkNotNull(config, "config"); // Per-route and per-host configuration overrides. @@ -119,7 +118,8 @@ public ServerInterceptor buildServerInterceptor( @Override public void shutdown() { - // TODO(sergiitk): besides shutting down everything, should there be a per-route destructor? + // TODO(sergiitk): [DESIGN] besides shutting down everything, should there + // be per-route interceptor destructors? RlqsClientPool oldClientPool = rlqsClientPoolRef.getAndUpdate(unused -> null); if (oldClientPool != null) { oldClientPool.shutdown(); @@ -135,6 +135,8 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { // Being shut down, return no interceptor. return null; } + // TODO(sergiitk): [DESIGN] Rlqs client should take the channel as an argument? + // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? rlqsClientPool.addClient(config.rlqsService()); // final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config); @@ -152,7 +154,7 @@ public Listener interceptCall( // - and interface to notify service interceptor on shutdown // - destroy channel when ref count 0 // potentially many RLQS Clients sharing a channel to grpc RLQS service - - // TODO(sergiitk): look up how cache is looked up + // TODO(sergiitk): [QUESTION] look up how cache is looked up // now we create filters every RPC. will be change in RBAC. // we need to avoid recreating filter when config doesn't change // m: trigger close() after we create new instances @@ -188,7 +190,7 @@ static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterPro builder.domain(rlqsFilterProto.getDomain()) .rlqsService(GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer())); - // TODO(sergiitk): bucket_matchers. + // TODO(sergiitk): [IMPL] bucket_matchers. return builder.build(); } @@ -197,7 +199,7 @@ static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterPro static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilterProtoOverride) throws ResourceInvalidException { RlqsFilterConfig.Builder builder = RlqsFilterConfig.builder(); - // TODO(sergiitk): bucket_matchers. + // TODO(sergiitk): [IMPL] bucket_matchers. return builder.domain(rlqsFilterProtoOverride.getDomain()).build(); } diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index 809d179abf0..fea71a4c54f 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -50,5 +50,5 @@ abstract static class Builder { abstract RlqsFilterConfig build(); } - // TODO(sergiitk): add rlqs_server, bucket_matchers. + // TODO(sergiitk): [IMPL] add rlqs_server, bucket_matchers. } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 596e029dcc3..108041f95bd 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -459,7 +459,7 @@ private void shutdown() { private void updateSelector() { Map> filterChainRouting = new HashMap<>(); - // TODO(sergiitk): is this a good place to reset interceptors? + // TODO(sergiitk): [QUESTION] is this a good place to reset interceptors? // for (FilterChain filterChain : savedRdsRoutingConfigRef.keySet()) { // if (!resourceName.equals(filterChain.httpConnectionManager().rdsName())) { // continue; diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java index 2336e20de60..658544a4291 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/GrpcService.java @@ -26,12 +26,12 @@ public abstract class GrpcService { public abstract String targetUri(); - // TODO(sergiitk): do we need this? + // TODO(sergiitk): [QUESTION] do we need this? // abstract String statPrefix(); - // TODO(sergiitk): channelCredentials - // TODO(sergiitk): callCredentials - // TODO(sergiitk): channelArgs + // TODO(sergiitk): [IMPL] channelCredentials + // TODO(sergiitk): [IMPL] callCredentials + // TODO(sergiitk): [IMPL] channelArgs /** Optional timeout duration for the gRPC request to the service. */ @Nullable @@ -54,10 +54,10 @@ public static GrpcService fromEnvoyProto( grpcServiceProto.getGoogleGrpc(); builder.targetUri(googleGrpcProto.getTargetUri()); - // TODO(sergiitk): channelCredentials - // TODO(sergiitk): callCredentials - // TODO(sergiitk): channelArgs - // TODO(sergiitk): statPrefix - (maybe) + // TODO(sergiitk): [IMPL] channelCredentials + // TODO(sergiitk): [IMPL] callCredentials + // TODO(sergiitk): [IMPL] channelArgs + // TODO(sergiitk): [IMPL] statPrefix - (maybe) return builder.build(); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index b6faeecdad4..f8f639fb03d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -31,6 +31,6 @@ final class RlqsClient { public void shutdown() { logger.log(Level.FINER, "Shutting down RlqsClient to {0}", targetUri); - // TODO(sergiitk): impl + // TODO(sergiitk): [IMPL] RlqsClient shutdown } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index 116f8e487f0..5ac01329bfe 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -32,10 +32,9 @@ public final class RlqsClientPool { private static final Logger logger = Logger.getLogger(RlqsClientPool.class.getName()); - // TODO(sergiitk): make a param? private static final int DEFAULT_CLEANUP_INTERVAL_SECONDS = 10; - // TODO(sergiitk): always in sync context? + // TODO(sergiitk): [QUESTION] always in sync context? private boolean shutdown; private final SynchronizationContext syncContext = new SynchronizationContext((thread, error) -> { String message = "Uncaught exception in RlqsClientPool SynchronizationContext. Panic!"; @@ -58,8 +57,8 @@ private RlqsClientPool(ScheduledExecutorService scheduler, int cleanupIntervalSe /** Creates an instance. */ public static RlqsClientPool newInstance(ScheduledExecutorService scheduler) { - // TODO(sergiitk): scheduler - consider using GrpcUtil.TIMER_SERVICE. - // TODO(sergiitk): note that the scheduler has a finite lifetime. + // TODO(sergiitk): [IMPL] scheduler - consider using GrpcUtil.TIMER_SERVICE. + // TODO(sergiitk): [IMPL] note that the scheduler has a finite lifetime. return new RlqsClientPool(scheduler, 0); } From caa1d133f690761512085791bd2fbd360043e8b8 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 26 Mar 2024 16:41:57 -0700 Subject: [PATCH 10/47] Basic RlqsBucketSettings and Matcher parsing --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 36 +++++++++++++++-- .../java/io/grpc/xds/RlqsFilterConfig.java | 8 +++- .../grpc/xds/internal/matchers/Matcher.java | 31 ++++++++++++++ .../xds/internal/matchers/MatcherList.java | 22 ++++++++++ .../grpc/xds/internal/matchers/OnMatch.java | 40 +++++++++++++++++++ .../xds/internal/rlqs/RlqsBucketSettings.java | 38 ++++++++++++++++++ 6 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 4fd7837a40f..7e1712ff091 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -20,9 +20,11 @@ import static io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; +import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; import io.grpc.Metadata; @@ -32,6 +34,10 @@ import io.grpc.ServerInterceptor; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.internal.matchers.Matcher; +import io.grpc.xds.internal.matchers.MatcherList; +import io.grpc.xds.internal.matchers.OnMatch; +import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import io.grpc.xds.internal.rlqs.RlqsClientPool; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -108,6 +114,9 @@ public ServerInterceptor buildServerInterceptor( if (!rlqsFilterOverride.domain().isEmpty()) { overrideBuilder.domain(rlqsFilterOverride.domain()); } + if (rlqsFilterOverride.bucketMatchers() != null) { + overrideBuilder.bucketMatchers(rlqsFilterOverride.bucketMatchers()); + } // Override bucket matchers if not null. rlqsFilterConfig = overrideBuilder.build(); } @@ -182,7 +191,7 @@ public Listener interceptCall( @VisibleForTesting static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) - throws ResourceInvalidException { + throws ResourceInvalidException, InvalidProtocolBufferException { RlqsFilterConfig.Builder builder = RlqsFilterConfig.builder(); if (rlqsFilterProto.getDomain().isEmpty()) { throw new ResourceInvalidException("RateLimitQuotaFilterConfig domain is required"); @@ -190,9 +199,29 @@ static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterPro builder.domain(rlqsFilterProto.getDomain()) .rlqsService(GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer())); - // TODO(sergiitk): [IMPL] bucket_matchers. + // TODO(sergiitk): [IMPL] actually parse, move to RlqsBucketSettings.fromProto() + RateLimitQuotaBucketSettings fallbackBucketSettingsProto = unpackAny( + rlqsFilterProto.getBucketMatchers().getOnNoMatch().getAction().getTypedConfig(), + RateLimitQuotaBucketSettings.class); + RlqsBucketSettings fallbackBucket = RlqsBucketSettings.create( + ImmutableMap.of("bucket_id", headers -> "hello"), + fallbackBucketSettingsProto.getReportingInterval()); + + // TODO(sergiitk): [IMPL] actually parse, move to Matcher.fromProto() + Matcher bucketMatchers = new Matcher() { + @Nullable + @Override + public MatcherList matcherList() { + return null; + } - return builder.build(); + @Override + public OnMatch onNoMatch() { + return OnMatch.ofAction(fallbackBucket); + } + }; + + return builder.bucketMatchers(bucketMatchers).build(); } @VisibleForTesting @@ -213,4 +242,3 @@ private static T unpackAny( return ((Any) message).unpack(clazz); } } - diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index fea71a4c54f..b9fe1c7e83e 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -19,6 +19,8 @@ import com.google.auto.value.AutoValue; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.internal.matchers.Matcher; +import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import javax.annotation.Nullable; /** Parsed RateLimitQuotaFilterConfig. */ @@ -35,6 +37,9 @@ public final String typeUrl() { @Nullable abstract GrpcService rlqsService(); + @Nullable + abstract Matcher bucketMatchers(); + public static Builder builder() { return new AutoValue_RlqsFilterConfig.Builder(); } @@ -47,8 +52,9 @@ abstract static class Builder { abstract Builder rlqsService(GrpcService rlqsService); + public abstract Builder bucketMatchers(Matcher matcher); + abstract RlqsFilterConfig build(); } - // TODO(sergiitk): [IMPL] add rlqs_server, bucket_matchers. } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java new file mode 100644 index 00000000000..7d8cd2ea220 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +import javax.annotation.Nullable; + +/** Unified Matcher API: xds.type.matcher.v3.Matcher. */ +public abstract class Matcher { + // TODO(sergiitk): [IMPL] iterator? + // TODO(sergiitk): [IMPL] public boolean matches(EvaluateArgs args) ? + + // TODO(sergiitk): [IMPL] AutoOneOf MatcherList, MatcherTree + @Nullable + public abstract MatcherList matcherList(); + + public abstract OnMatch onNoMatch(); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java b/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java new file mode 100644 index 00000000000..35062a8c161 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +/** Unified Matcher API: xds.type.matcher.v3.Matcher.MatcherList. */ +public class MatcherList { + +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java b/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java new file mode 100644 index 00000000000..5f845337f81 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + + +import com.google.auto.value.AutoOneOf; + +/** Unified Matcher API: xds.type.matcher.v3.Matcher.OnMatch. */ +@AutoOneOf(OnMatch.Kind.class) +public abstract class OnMatch { + public enum Kind { MATCHER, ACTION } + + public abstract Kind getKind(); + + public abstract Matcher matcher(); + + public abstract T action(); + + public static OnMatch ofMatcher(Matcher matcher) { + return AutoOneOf_OnMatch.matcher(matcher); + } + + public static OnMatch ofAction(T result) { + return AutoOneOf_OnMatch.action(result); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java new file mode 100644 index 00000000000..dc9d31fe5c2 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Duration; +import io.grpc.Metadata; + +@AutoValue +public abstract class RlqsBucketSettings { + + public abstract ImmutableMap> bucketIdBuilder(); + + public abstract Duration reportingInterval(); + + public static RlqsBucketSettings create( + ImmutableMap> bucketIdBuilder, + Duration reportingInterval) { + return new AutoValue_RlqsBucketSettings(bucketIdBuilder, reportingInterval); + } + +} From b25efa9da2c2d1b96d6a89afc2d0d8d550936e75 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 26 Mar 2024 17:11:34 -0700 Subject: [PATCH 11/47] Minimal CelMatcher --- .../xds/internal/matchers/CelMatcher.java | 42 +++++++++++++++++++ .../xds/internal/matchers/HttpMatchInput.java | 28 +++++++++++++ .../xds/internal/rlqs/RlqsBucketSettings.java | 7 ++-- .../xds/internal/matchers/CelMatcherTest.java | 37 ++++++++++++++++ 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java new file mode 100644 index 00000000000..24667512d55 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +import java.util.function.Predicate; + +/** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ +public class CelMatcher implements Predicate { + private final String description; + + public CelMatcher(String description) { + // TODO(sergiitk): cache parsed CEL expressions + this.description = description != null ? description : ""; + } + + public String description() { + return description; + } + + @Override + public boolean test(HttpMatchInput httpMatchInput) { + if (httpMatchInput.headers().keys().isEmpty()) { + return false; + } + // TODO(sergiitk): [IMPL] convert headers to cel args + return true; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java new file mode 100644 index 00000000000..08cdfacab4f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package io.grpc.xds.internal.matchers; + +import com.google.auto.value.AutoValue; +import io.grpc.Metadata; + +@AutoValue +public abstract class HttpMatchInput { + public abstract Metadata headers(); + // TODO(sergiitk): [IMPL] consider + // public abstract ServerCall serverCall(); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index dc9d31fe5c2..8fb759817bc 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -20,19 +20,18 @@ import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; -import io.grpc.Metadata; +import io.grpc.xds.internal.matchers.HttpMatchInput; @AutoValue public abstract class RlqsBucketSettings { - public abstract ImmutableMap> bucketIdBuilder(); + public abstract ImmutableMap> bucketIdBuilder(); public abstract Duration reportingInterval(); public static RlqsBucketSettings create( - ImmutableMap> bucketIdBuilder, + ImmutableMap> bucketIdBuilder, Duration reportingInterval) { return new AutoValue_RlqsBucketSettings(bucketIdBuilder, reportingInterval); } - } diff --git a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java new file mode 100644 index 00000000000..4c266138bd1 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelMatcherTest { + + @Test + public void construct() { + String description = "Optional description"; + CelMatcher matcher = new CelMatcher(description); + assertThat(matcher.description()).isEqualTo(description); + + matcher = new CelMatcher(null); + assertThat(matcher.description()).isEqualTo(""); + } +} From 9fd26b7d759a9f5e069e5fdca35c6ca21a94dfde Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 26 Mar 2024 18:24:19 -0700 Subject: [PATCH 12/47] basic cel-java integration/test --- gradle/libs.versions.toml | 1 + xds/build.gradle | 1 + .../xds/internal/matchers/CelMatcher.java | 35 +++++++++++++--- .../xds/internal/matchers/CelMatcherTest.java | 42 +++++++++++++++++-- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9f9c5350e1..cf13ba5b614 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" +dev-cel = "dev.cel:cel:0.6.0" # error-prone 2.31.0+ blocked on https://github.com/grpc/grpc-java/issues/10152 # It breaks Bazel (ArrayIndexOutOfBoundsException in turbine) and Dexing ("D8: # java.lang.NullPointerException"). We can trivially upgrade the Bazel CI to diff --git a/xds/build.gradle b/xds/build.gradle index e09b42d06a9..570fa88b333 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -52,6 +52,7 @@ dependencies { project(':grpc-services'), project(':grpc-auth'), project(path: ':grpc-alts', configuration: 'shadow'), + libraries.dev.cel, libraries.guava, libraries.gson, libraries.re2j, diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java index 24667512d55..fac76106751 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java @@ -16,27 +16,52 @@ package io.grpc.xds.internal.matchers; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; import java.util.function.Predicate; /** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ public class CelMatcher implements Predicate { + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory.standardCelRuntimeBuilder().build(); + + private final CelRuntime.Program program; private final String description; - public CelMatcher(String description) { - // TODO(sergiitk): cache parsed CEL expressions + private CelMatcher(CelAbstractSyntaxTree ast, String description) throws CelEvaluationException { + this.program = CEL_RUNTIME.createProgram(checkNotNull(ast)); this.description = description != null ? description : ""; } + public static CelMatcher create(CelAbstractSyntaxTree ast) throws CelEvaluationException { + return new CelMatcher(ast, null); + } + + public static CelMatcher create(CelAbstractSyntaxTree ast, String description) + throws CelEvaluationException { + return new CelMatcher(ast, description); + } + public String description() { return description; } @Override public boolean test(HttpMatchInput httpMatchInput) { - if (httpMatchInput.headers().keys().isEmpty()) { + // if (httpMatchInput.headers().keys().isEmpty()) { + // return false; + // } + // TODO(sergiitk): [IMPL] convert headers to cel args + try { + return (boolean) program.eval(ImmutableMap.of("my_var", "Hello World")); + } catch (CelEvaluationException e) { + // TODO(sergiitk): [IMPL] log cel error? return false; } - // TODO(sergiitk): [IMPL] convert headers to cel args - return true; } } diff --git a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java index 4c266138bd1..95e2327fc35 100644 --- a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java @@ -18,20 +18,54 @@ import static com.google.common.truth.Truth.assertThat; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationResult; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelEvaluationException; +import io.grpc.Metadata; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class CelMatcherTest { + // Construct the compilation and runtime environments. + // These instances are immutable and thus trivially thread-safe and amenable to caching. + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder().addVar("my_var", SimpleType.STRING).build(); + private static final CelValidationResult celProg1 = + CEL_COMPILER.compile("type(my_var) == string"); + + CelAbstractSyntaxTree ast1; + CelMatcher matcher1; + + private static final HttpMatchInput fakeInput = new HttpMatchInput() { + @Override + public Metadata headers() { + return new Metadata(); + } + }; + + @Before + public void setUp() throws Exception { + ast1 = celProg1.getAst(); + matcher1 = CelMatcher.create(ast1); + } @Test - public void construct() { + public void construct() throws CelEvaluationException { + assertThat(matcher1.description()).isEqualTo(""); + String description = "Optional description"; - CelMatcher matcher = new CelMatcher(description); + CelMatcher matcher = CelMatcher.create(ast1, description); assertThat(matcher.description()).isEqualTo(description); + } - matcher = new CelMatcher(null); - assertThat(matcher.description()).isEqualTo(""); + @Test + public void testProgTrue() { + assertThat(matcher1.test(fakeInput)).isTrue(); } } From b870c02e76f47596780d73384bd220cea3d58f36 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 4 Sep 2024 06:00:43 -0700 Subject: [PATCH 13/47] Implement GrpcCelEnvironment and MetadataHelper --- .../io/grpc/xds/internal/MetadataHelper.java | 63 +++++++++++++++++++ .../xds/internal/matchers/CelMatcher.java | 16 +---- .../internal/matchers/GrpcCelEnvironment.java | 61 ++++++++++++++++++ .../xds/internal/matchers/HttpMatchInput.java | 4 +- .../xds/internal/matchers/CelMatcherTest.java | 28 ++++++++- 5 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java diff --git a/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java b/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java new file mode 100644 index 00000000000..eed684fe255 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; +import io.grpc.Metadata; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + + +public class MetadataHelper { + public static ImmutableMap metadataToHeaders(Metadata metadata) { + return metadata.keys().stream().collect(ImmutableMap.toImmutableMap( + headerName -> headerName, + headerName -> Strings.nullToEmpty(deserializeHeader(metadata, headerName)))); + } + + @Nullable + public static String deserializeHeader(Metadata metadata, String headerName) { + if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + Metadata.Key key; + try { + key = Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER); + } catch (IllegalArgumentException e) { + return null; + } + Iterable values = metadata.getAll(key); + if (values == null) { + return null; + } + List encoded = new ArrayList<>(); + for (byte[] v : values) { + encoded.add(BaseEncoding.base64().omitPadding().encode(v)); + } + return String.join(",", encoded); + } + Metadata.Key key; + try { + key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); + } catch (IllegalArgumentException e) { + return null; + } + Iterable values = metadata.getAll(key); + return values == null ? null : String.join(",", values); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java index fac76106751..8659d6845ae 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java @@ -18,23 +18,18 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.collect.ImmutableMap; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime; -import dev.cel.runtime.CelRuntimeFactory; import java.util.function.Predicate; /** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ public class CelMatcher implements Predicate { - private static final CelRuntime CEL_RUNTIME = - CelRuntimeFactory.standardCelRuntimeBuilder().build(); - private final CelRuntime.Program program; + private final GrpcCelEnvironment program; private final String description; private CelMatcher(CelAbstractSyntaxTree ast, String description) throws CelEvaluationException { - this.program = CEL_RUNTIME.createProgram(checkNotNull(ast)); + this.program = new GrpcCelEnvironment(checkNotNull(ast)); this.description = description != null ? description : ""; } @@ -57,11 +52,6 @@ public boolean test(HttpMatchInput httpMatchInput) { // return false; // } // TODO(sergiitk): [IMPL] convert headers to cel args - try { - return (boolean) program.eval(ImmutableMap.of("my_var", "Hello World")); - } catch (CelEvaluationException e) { - // TODO(sergiitk): [IMPL] log cel error? - return false; - } + return program.eval(httpMatchInput.serverCall(), httpMatchInput.headers()); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java b/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java new file mode 100644 index 00000000000..ae67f5bbfcd --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.types.SimpleType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.xds.internal.MetadataHelper; + +/** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ +public class GrpcCelEnvironment { + private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory + .standardCelRuntimeBuilder() + .setOptions(CelOptions.current().comprehensionMaxIterations(0).build()) + .build(); + + private final CelRuntime.Program program; + + GrpcCelEnvironment(CelAbstractSyntaxTree ast) throws CelEvaluationException { + if (ast.getResultType() != SimpleType.BOOL) { + throw new CelEvaluationException("Expected bool return type"); + } + this.program = CEL_RUNTIME.createProgram(ast); + } + + public boolean eval(ServerCall serverCall, Metadata metadata) { + ImmutableMap.Builder requestBuilder = ImmutableMap.builder() + .put("method", "POST") + .put("host", Strings.nullToEmpty(serverCall.getAuthority())) + .put("path", "/" + serverCall.getMethodDescriptor().getFullMethodName()) + .put("headers", MetadataHelper.metadataToHeaders(metadata)); + // TODO(sergiitk): handle other pseudo-headers + try { + return (boolean) program.eval(ImmutableMap.of("request", requestBuilder.build())); + } catch (CelEvaluationException e) { + throw Status.fromThrowable(e).asRuntimeException(); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java index 08cdfacab4f..2903cdc23aa 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java @@ -19,10 +19,12 @@ import com.google.auto.value.AutoValue; import io.grpc.Metadata; +import io.grpc.ServerCall; @AutoValue public abstract class HttpMatchInput { public abstract Metadata headers(); + // TODO(sergiitk): [IMPL] consider - // public abstract ServerCall serverCall(); + public abstract ServerCall serverCall(); } diff --git a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java index 95e2327fc35..782ce57724f 100644 --- a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java @@ -25,6 +25,11 @@ import dev.cel.compiler.CelCompilerFactory; import dev.cel.runtime.CelEvaluationException; import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.NoopServerCall; +import io.grpc.ServerCall; +import io.grpc.StringMarshaller; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,9 +40,12 @@ public class CelMatcherTest { // Construct the compilation and runtime environments. // These instances are immutable and thus trivially thread-safe and amenable to caching. private static final CelCompiler CEL_COMPILER = - CelCompilerFactory.standardCelCompilerBuilder().addVar("my_var", SimpleType.STRING).build(); + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("request", SimpleType.ANY) + .setResultType(SimpleType.BOOL) + .build(); private static final CelValidationResult celProg1 = - CEL_COMPILER.compile("type(my_var) == string"); + CEL_COMPILER.compile("request.method == \"POST\""); CelAbstractSyntaxTree ast1; CelMatcher matcher1; @@ -47,6 +55,22 @@ public class CelMatcherTest { public Metadata headers() { return new Metadata(); } + + @Override public ServerCall serverCall() { + final MethodDescriptor method = + MethodDescriptor.newBuilder() + .setType(MethodType.UNKNOWN) + .setFullMethodName("service/method") + .setRequestMarshaller(new StringMarshaller()) + .setResponseMarshaller(new StringMarshaller()) + .build(); + return new NoopServerCall() { + @Override + public MethodDescriptor getMethodDescriptor() { + return method; + } + }; + } }; @Before From dfca019b3e91768312863a16898d4ee91d320b8c Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 6 Sep 2024 10:57:13 -0700 Subject: [PATCH 14/47] Use dev.cel:runtime in the prod code --- gradle/libs.versions.toml | 3 ++- xds/build.gradle | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cf13ba5b614..247215471df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,8 @@ commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -dev-cel = "dev.cel:cel:0.6.0" +dev-cel-compiler = "dev.cel:compiler:0.6.0" +dev-cel-runtime = "dev.cel:runtime:0.6.0" # error-prone 2.31.0+ blocked on https://github.com/grpc/grpc-java/issues/10152 # It breaks Bazel (ArrayIndexOutOfBoundsException in turbine) and Dexing ("D8: # java.lang.NullPointerException"). We can trivially upgrade the Bazel CI to diff --git a/xds/build.gradle b/xds/build.gradle index 570fa88b333..028d614c94b 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -52,7 +52,7 @@ dependencies { project(':grpc-services'), project(':grpc-auth'), project(path: ':grpc-alts', configuration: 'shadow'), - libraries.dev.cel, + libraries.dev.cel.runtime, libraries.guava, libraries.gson, libraries.re2j, @@ -71,7 +71,8 @@ dependencies { compileOnly libraries.netty.transport.epoll testImplementation project(':grpc-testing'), - project(':grpc-testing-proto') + project(':grpc-testing-proto'), + libraries.dev.cel.compiler testImplementation (libraries.netty.transport.epoll) { artifact { classifier = "linux-x86_64" From 396346989b10cecca5ff9a6e5fbe4596c1e5591c Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 16 Sep 2024 15:36:51 -0700 Subject: [PATCH 15/47] Add RlqsClientPool/RlqsClient/RlqsApiClient classes --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 69 +++++---- .../java/io/grpc/xds/RlqsFilterConfig.java | 11 +- .../java/io/grpc/xds/client/Bootstrapper.java | 16 ++ .../xds/internal/matchers/HttpMatchInput.java | 4 + .../grpc/xds/internal/matchers/Matcher.java | 8 +- .../xds/internal/matchers/MatcherList.java | 2 +- .../grpc/xds/internal/matchers/OnMatch.java | 11 +- .../grpc/xds/internal/rlqs/RlqsApiClient.java | 145 ++++++++++++++++++ .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 56 +++++++ .../xds/internal/rlqs/RlqsBucketCache.java | 38 +++++ .../grpc/xds/internal/rlqs/RlqsBucketId.java | 40 +++++ .../xds/internal/rlqs/RlqsBucketSettings.java | 4 + .../io/grpc/xds/internal/rlqs/RlqsClient.java | 38 ++++- .../xds/internal/rlqs/RlqsClientPool.java | 49 ++++-- 14 files changed, 432 insertions(+), 59 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 7e1712ff091..2d3d68a353d 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -32,12 +32,16 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import io.grpc.Status; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.matchers.MatcherList; import io.grpc.xds.internal.matchers.OnMatch; +import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; +import io.grpc.xds.internal.rlqs.RlqsClient; import io.grpc.xds.internal.rlqs.RlqsClientPool; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -144,17 +148,14 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { // Being shut down, return no interceptor. return null; } - // TODO(sergiitk): [DESIGN] Rlqs client should take the channel as an argument? - // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? - rlqsClientPool.addClient(config.rlqsService()); + final RlqsClient rlqsClient = rlqsClientPool.getOrCreateRlqsClient(config); - // final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config); return new ServerInterceptor() { @Override public Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { // Notes: - // map domain() -> an incarnation of bucket matchers, f.e. new RlqsEngine(domain, matchers). + // map domain() -> an incarnation of bucket matchers, f.e. new RlqsClient(domain, matchers). // shared resource holder, acquire every rpc // Store RLQS Client or channel in the config as a reference - FilterConfig config ref // when parse. @@ -171,20 +172,13 @@ public Listener interceptCall( // AI: follow up with Eric on how cache is shared, this changes if we need to cache // interceptor // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. - - // Example: - // AuthDecision authResult = authEngine.evaluate(headers, call); - // if (logger.isLoggable(Level.FINE)) { - // logger.log(Level.FINE, - // "Authorization result for serverCall {0}: {1}, matching policy: {2}.", - // new Object[]{call, authResult.decision(), authResult.matchingPolicyName()}); - // } - // if (GrpcAuthorizationEngine.Action.DENY.equals(authResult.decision())) { - // Status status = Status.PERMISSION_DENIED.withDescription("Access Denied"); - // call.close(status, new Metadata()); - // return new ServerCall.Listener(){}; - // } - return next.startCall(call, headers); + RateLimitResult result = rlqsClient.evaluate(HttpMatchInput.create(headers, call)); + if (RateLimitResult.ALLOWED.equals(result)) { + return next.startCall(call, headers); + } + Status status = Status.UNAVAILABLE.withDescription(""); + call.close(status, new Metadata()); + return new ServerCall.Listener(){}; } }; } @@ -208,22 +202,35 @@ static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterPro fallbackBucketSettingsProto.getReportingInterval()); // TODO(sergiitk): [IMPL] actually parse, move to Matcher.fromProto() - Matcher bucketMatchers = new Matcher() { - @Nullable - @Override - public MatcherList matcherList() { - return null; - } - - @Override - public OnMatch onNoMatch() { - return OnMatch.ofAction(fallbackBucket); - } - }; + Matcher bucketMatchers = new RlqsMatcher(fallbackBucket); return builder.bucketMatchers(bucketMatchers).build(); } + static class RlqsMatcher extends Matcher { + private final RlqsBucketSettings fallbackBucket; + + RlqsMatcher(RlqsBucketSettings fallbackBucket) { + this.fallbackBucket = fallbackBucket; + } + + @Nullable + @Override + public MatcherList matcherList() { + return null; + } + + @Override + public OnMatch onNoMatch() { + return OnMatch.ofAction(fallbackBucket); + } + + @Override + public RlqsBucketSettings match(HttpMatchInput input) { + return null; + } + } + @VisibleForTesting static RlqsFilterConfig parseRlqsFilterOverride(RateLimitQuotaOverride rlqsFilterProtoOverride) throws ResourceInvalidException { diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index b9fe1c7e83e..1ccc911c9ac 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -19,26 +19,27 @@ import com.google.auto.value.AutoValue; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import javax.annotation.Nullable; /** Parsed RateLimitQuotaFilterConfig. */ @AutoValue -abstract class RlqsFilterConfig implements FilterConfig { +public abstract class RlqsFilterConfig implements FilterConfig { @Override public final String typeUrl() { return RlqsFilter.TYPE_URL; } - abstract String domain(); + public abstract String domain(); @Nullable - abstract GrpcService rlqsService(); + public abstract GrpcService rlqsService(); @Nullable - abstract Matcher bucketMatchers(); + public abstract Matcher bucketMatchers(); public static Builder builder() { return new AutoValue_RlqsFilterConfig.Builder(); @@ -52,7 +53,7 @@ abstract static class Builder { abstract Builder rlqsService(GrpcService rlqsService); - public abstract Builder bucketMatchers(Matcher matcher); + public abstract Builder bucketMatchers(Matcher matcher); abstract RlqsFilterConfig build(); } diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 90babd1e8d0..a2c0cf8a1ef 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.ChannelCredentials; import io.grpc.Internal; import io.grpc.xds.client.EnvoyProtoData.Node; import java.util.List; @@ -77,6 +78,21 @@ public static ServerInfo create( } } + /** + * TODO(sergiitk): description. + */ + @AutoValue + @Internal + public abstract static class RemoteServerInfo { + public abstract String target(); + + public abstract ChannelCredentials channelCredentials(); + + public static RemoteServerInfo create(String target, ChannelCredentials channelCredentials) { + return new AutoValue_Bootstrapper_RemoteServerInfo(target, channelCredentials); + } + } + /** * Data class containing Certificate provider information: the plugin-name and an opaque * Map that represents the config for that plugin. diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java index 2903cdc23aa..04dd9cbe4e5 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java @@ -27,4 +27,8 @@ public abstract class HttpMatchInput { // TODO(sergiitk): [IMPL] consider public abstract ServerCall serverCall(); + + public static HttpMatchInput create(Metadata headers, ServerCall serverCall) { + return new AutoValue_HttpMatchInput(headers, serverCall); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java index 7d8cd2ea220..6740406d3c0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/Matcher.java @@ -19,13 +19,15 @@ import javax.annotation.Nullable; /** Unified Matcher API: xds.type.matcher.v3.Matcher. */ -public abstract class Matcher { +public abstract class Matcher { // TODO(sergiitk): [IMPL] iterator? // TODO(sergiitk): [IMPL] public boolean matches(EvaluateArgs args) ? // TODO(sergiitk): [IMPL] AutoOneOf MatcherList, MatcherTree @Nullable - public abstract MatcherList matcherList(); + public abstract MatcherList matcherList(); - public abstract OnMatch onNoMatch(); + public abstract OnMatch onNoMatch(); + + public abstract ResultT match(InputT input); } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java b/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java index 35062a8c161..76fb2d4b632 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/MatcherList.java @@ -17,6 +17,6 @@ package io.grpc.xds.internal.matchers; /** Unified Matcher API: xds.type.matcher.v3.Matcher.MatcherList. */ -public class MatcherList { +public class MatcherList { } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java b/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java index 5f845337f81..5e3609ab72a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/OnMatch.java @@ -21,20 +21,21 @@ /** Unified Matcher API: xds.type.matcher.v3.Matcher.OnMatch. */ @AutoOneOf(OnMatch.Kind.class) -public abstract class OnMatch { +public abstract class OnMatch { public enum Kind { MATCHER, ACTION } public abstract Kind getKind(); - public abstract Matcher matcher(); + public abstract Matcher matcher(); - public abstract T action(); + public abstract ResultT action(); - public static OnMatch ofMatcher(Matcher matcher) { + public static OnMatch ofMatcher( + Matcher matcher) { return AutoOneOf_OnMatch.matcher(matcher); } - public static OnMatch ofAction(T result) { + public static OnMatch ofAction(ResultT result) { return AutoOneOf_OnMatch.action(result); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java new file mode 100644 index 00000000000..ca6ee9017c0 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import com.google.protobuf.Duration; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaServiceGrpc; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaServiceGrpc.RateLimitQuotaServiceStub; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports.BucketQuotaUsage; +import io.envoyproxy.envoy.type.v3.RateLimitStrategy; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.StreamObserver; +import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class RlqsApiClient { + private static final Logger logger = Logger.getLogger(RlqsApiClient.class.getName()); + + private final RemoteServerInfo serverInfo; + private final String domain; + private final RlqsApiClientInternal rlqsApiClient; + private final RlqsBucketCache bucketCache; + + RlqsApiClient(RemoteServerInfo serverInfo, String domain, RlqsBucketCache bucketCache) { + this.serverInfo = serverInfo; + this.domain = domain; + this.rlqsApiClient = new RlqsApiClientInternal(serverInfo); + this.bucketCache = bucketCache; + } + + void sendInitialUsageReport(RlqsBucket bucket) { + rlqsApiClient.reportUsage(RateLimitQuotaUsageReports.newBuilder() + .setDomain(domain) + .addBucketQuotaUsages(toUsageReport(bucket)) + .build()); + } + + + void sendUsageReports() { + RateLimitQuotaUsageReports.Builder reports = RateLimitQuotaUsageReports.newBuilder(); + for (RlqsBucket bucket : bucketCache.getBucketsToReport()) { + reports.addBucketQuotaUsages(toUsageReport(bucket)); + } + rlqsApiClient.reportUsage(reports.build()); + } + + void abandonBucket(RlqsBucketId bucketId) { + bucketCache.deleteBucket(bucketId); + } + + void updateBucketAssignment( + RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, Duration duration) { + // Deadline.after(Durations.toMillis(ttl), TimeUnit.MILLISECONDS); + } + + BucketQuotaUsage toUsageReport(RlqsBucket bucket) { + return null; + } + + public void shutdown() { + logger.log(Level.FINER, "Shutting down RlqsApiClient to {0}", serverInfo.target()); + // TODO(sergiitk): [IMPL] RlqsApiClient shutdown + } + + private class RlqsApiClientInternal { + private final ManagedChannel channel; + private final RateLimitQuotaServiceStub stub; + private final ClientCallStreamObserver clientCallStream; + + RlqsApiClientInternal(RemoteServerInfo serverInfo) { + channel = Grpc.newChannelBuilder(serverInfo.target(), serverInfo.channelCredentials()) + .keepAliveTime(10, TimeUnit.SECONDS) + .keepAliveWithoutCalls(true) + .build(); + // keepalive? + // TODO(sergiitk): [IMPL] Manage State changes? + stub = RateLimitQuotaServiceGrpc.newStub(channel); + clientCallStream = (ClientCallStreamObserver) + stub.streamRateLimitQuotas(new RlqsStreamObserver()); + // TODO(sergiitk): [IMPL] set on ready handler? + } + + void reportUsage(RateLimitQuotaUsageReports usageReports) { + clientCallStream.onNext(usageReports); + } + + /** + * RLQS Stream observer. + * + *

See {@link io.grpc.alts.internal.AltsHandshakerStub.Reader} for examples. + * See {@link io.grpc.stub.ClientResponseObserver} for flow control examples. + */ + private class RlqsStreamObserver implements StreamObserver { + @Override + public void onNext(RateLimitQuotaResponse response) { + for (BucketAction bucketAction : response.getBucketActionList()) { + switch (bucketAction.getBucketActionCase()) { + case ABANDON_ACTION: + abandonBucket(RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId())); + break; + case QUOTA_ASSIGNMENT_ACTION: + QuotaAssignmentAction quotaAssignmentAction = bucketAction.getQuotaAssignmentAction(); + updateBucketAssignment(RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId()), + quotaAssignmentAction.getRateLimitStrategy(), + quotaAssignmentAction.getAssignmentTimeToLive()); + break; + default: + // TODO(sergiitk): error + } + } + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onCompleted() { + + } + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java new file mode 100644 index 00000000000..d81eaa75cdc --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +public class RlqsBucket { + public enum RateLimitResult { + ALLOWED, DENIED + } + + private final RlqsBucketId bucketId; + private long numRequestsAllowed = 0; + private long numRequestsDenied = 0; + // last_report_time + // last_assignment_time + + RlqsBucket(RlqsBucketId bucketId, RlqsBucketSettings bucketSettings) { + this.bucketId = bucketId; + } + + RateLimitResult rateLimit() { + // TODO(sergiitk): impl + numRequestsAllowed += 1; + return RateLimitResult.ALLOWED; + } + + void reset() { + numRequestsAllowed = 0; + numRequestsDenied = 0; + } + + public RlqsBucketId getBucketId() { + return bucketId; + } + + public long getNumRequestsDenied() { + return numRequestsDenied; + } + + public long getNumRequestsAllowed() { + return numRequestsAllowed; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java new file mode 100644 index 00000000000..21b7f970160 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + + +import com.google.common.collect.ImmutableList; + +final class RlqsBucketCache { + + RlqsBucket getBucket(RlqsBucketId bucketId) { + return null; + } + + void insertBucket(RlqsBucket bucket) { + } + + void deleteBucket(RlqsBucketId bucketId) { + } + + + public ImmutableList getBucketsToReport() { + return ImmutableList.of(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java new file mode 100644 index 00000000000..15eccfe2d6f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +@AutoValue +public abstract class RlqsBucketId { + public abstract ImmutableMap bucketId(); + + public static RlqsBucketId create(ImmutableMap bucketId) { + return new AutoValue_RlqsBucketId(bucketId); + } + + public static RlqsBucketId fromEnvoyProto( + io.envoyproxy.envoy.service.rate_limit_quota.v3.BucketId envoyProto) { + ImmutableMap.Builder bucketId = ImmutableMap.builder(); + for (Map.Entry entry : envoyProto.getBucketMap().entrySet()) { + bucketId.put(entry.getKey(), entry.getValue()); + } + return RlqsBucketId.create(bucketId.build()); + } + +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index 8fb759817bc..f49d2737900 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -27,6 +27,10 @@ public abstract class RlqsBucketSettings { public abstract ImmutableMap> bucketIdBuilder(); + public RlqsBucketId toBucketId(HttpMatchInput input) { + return null; + } + public abstract Duration reportingInterval(); public static RlqsBucketSettings create( diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index f8f639fb03d..1fe3e72d4c8 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -16,21 +16,49 @@ package io.grpc.xds.internal.rlqs; +import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.internal.matchers.HttpMatchInput; +import io.grpc.xds.internal.matchers.Matcher; +import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import java.util.logging.Level; import java.util.logging.Logger; -final class RlqsClient { +public class RlqsClient { private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); - private final String targetUri; + private final RlqsApiClient rlqsApiClient; + private final Matcher bucketMatchers; + private final RlqsBucketCache bucketCache; + private final String clientHash; - RlqsClient(String targetUri) { - this.targetUri = targetUri; + public RlqsClient( + RemoteServerInfo rlqsServer, String domain, + Matcher bucketMatchers, String clientHash) { + this.bucketMatchers = bucketMatchers; + this.clientHash = clientHash; + bucketCache = new RlqsBucketCache(); + rlqsApiClient = new RlqsApiClient(rlqsServer, domain, bucketCache); } + public RateLimitResult evaluate(HttpMatchInput input) { + RlqsBucketSettings bucketSettings = bucketMatchers.match(input); + RlqsBucketId bucketId = bucketSettings.toBucketId(input); + RlqsBucket bucket = bucketCache.getBucket(bucketId); + RateLimitResult rateLimitResult; + if (bucket != null) { + return bucket.rateLimit(); + } + bucket = new RlqsBucket(bucketId, bucketSettings); + rateLimitResult = bucket.rateLimit(); + bucketCache.insertBucket(bucket); + rlqsApiClient.sendInitialUsageReport(bucket); + // register tickers + return rateLimitResult; + } public void shutdown() { - logger.log(Level.FINER, "Shutting down RlqsClient to {0}", targetUri); // TODO(sergiitk): [IMPL] RlqsClient shutdown + logger.log(Level.FINER, "Shutting down RlqsClient with hash {0}", clientHash); + rlqsApiClient.shutdown(); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index 5ac01329bfe..92997276487 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -20,12 +20,17 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.Sets; +import com.google.common.util.concurrent.SettableFuture; +import io.grpc.InsecureChannelCredentials; import io.grpc.SynchronizationContext; -import io.grpc.xds.internal.datatype.GrpcService; +import io.grpc.xds.RlqsFilterConfig; +import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; @@ -67,9 +72,9 @@ public void run() { if (shutdown) { return; } - for (String targetUri : clientsToShutdown) { - clientPool.get(targetUri).shutdown(); - clientPool.remove(targetUri); + for (String clientHash : clientsToShutdown) { + clientPool.get(clientHash).shutdown(); + clientPool.remove(clientHash); } clientsToShutdown.clear(); }; @@ -81,18 +86,44 @@ public void shutdown() { shutdown = true; logger.log(Level.FINER, "Shutting down RlqsClientPool"); clientsToShutdown.clear(); - for (String targetUri : clientPool.keySet()) { - clientPool.get(targetUri).shutdown(); + for (String clientHash : clientPool.keySet()) { + clientPool.get(clientHash).shutdown(); } clientPool.clear(); }); } - public void addClient(GrpcService rlqsService) { + public RlqsClient getOrCreateRlqsClient(RlqsFilterConfig config) { + final SettableFuture future = SettableFuture.create(); + final String clientHash = makeRlqsClientHash(config); + syncContext.execute(() -> { - RlqsClient rlqsClient = new RlqsClient(rlqsService.targetUri()); - clientPool.put(rlqsService.targetUri(), rlqsClient); + if (clientPool.containsKey(clientHash)) { + future.set(clientPool.get(clientHash)); + return; + } + // TODO(sergiitk): [IMPL] get from bootstrap. + RemoteServerInfo rlqsServer = RemoteServerInfo.create(config.rlqsService().targetUri(), + InsecureChannelCredentials.create()); + RlqsClient rlqsClient = + new RlqsClient(rlqsServer, config.domain(), config.bucketMatchers(), clientHash); + + clientPool.put(clientHash, rlqsClient); + future.set(clientPool.get(clientHash)); }); + try { + // TODO(sergiitk): [IMPL] clarify time + return future.get(1, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + // TODO(sergiitk): [IMPL] handle properly + throw new RuntimeException(e); + } + } + + private String makeRlqsClientHash(RlqsFilterConfig config) { + // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? + // TODO(sergiitk): [IMPL] Hash buckets + return config.rlqsService().targetUri() + config.domain(); } /** From e8691ef2d3ea580733451d2545f48f5911aa8b69 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 15:22:36 -0700 Subject: [PATCH 16/47] Draft bucket processing logic --- .../grpc/xds/internal/rlqs/RlqsApiClient.java | 19 +++++++++++++++---- .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 1 + .../io/grpc/xds/internal/rlqs/RlqsClient.java | 11 ++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java index ca6ee9017c0..c646c42ea44 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java @@ -30,6 +30,7 @@ import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,18 +50,27 @@ public final class RlqsApiClient { this.bucketCache = bucketCache; } - void sendInitialUsageReport(RlqsBucket bucket) { + RateLimitResult processFirstBucketRequest(RlqsBucket bucket) { + bucketCache.insertBucket(bucket); + // Register first request to the bucket for the initial report. + RateLimitResult rateLimitResult = bucket.rateLimit(); + + // Send initial usage report. + BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); + bucket.reset(); rlqsApiClient.reportUsage(RateLimitQuotaUsageReports.newBuilder() .setDomain(domain) - .addBucketQuotaUsages(toUsageReport(bucket)) + .addBucketQuotaUsages(bucketQuotaUsage) .build()); + return rateLimitResult; } - void sendUsageReports() { RateLimitQuotaUsageReports.Builder reports = RateLimitQuotaUsageReports.newBuilder(); for (RlqsBucket bucket : bucketCache.getBucketsToReport()) { - reports.addBucketQuotaUsages(toUsageReport(bucket)); + BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); + bucket.reset(); + reports.addBucketQuotaUsages(bucketQuotaUsage); } rlqsApiClient.reportUsage(reports.build()); } @@ -75,6 +85,7 @@ void updateBucketAssignment( } BucketQuotaUsage toUsageReport(RlqsBucket bucket) { + // TODO(sergiitk): consider moving to RlqsBucket, and adding something like reportAndReset return null; } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index d81eaa75cdc..4f15b1799c8 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -22,6 +22,7 @@ public enum RateLimitResult { } private final RlqsBucketId bucketId; + // TODO(sergiitk): consider immutable report structure private long numRequestsAllowed = 0; private long numRequestsDenied = 0; // last_report_time diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 1fe3e72d4c8..1101d48515a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -44,18 +44,19 @@ public RateLimitResult evaluate(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); RlqsBucket bucket = bucketCache.getBucket(bucketId); - RateLimitResult rateLimitResult; if (bucket != null) { return bucket.rateLimit(); } bucket = new RlqsBucket(bucketId, bucketSettings); - rateLimitResult = bucket.rateLimit(); - bucketCache.insertBucket(bucket); - rlqsApiClient.sendInitialUsageReport(bucket); - // register tickers + RateLimitResult rateLimitResult = rlqsApiClient.processFirstBucketRequest(bucket); + // TODO(sergiitk): register tickers + registerTimers(bucket, bucketSettings); return rateLimitResult; } + private void registerTimers(RlqsBucket bucket, RlqsBucketSettings bucketSettings) { + } + public void shutdown() { // TODO(sergiitk): [IMPL] RlqsClient shutdown logger.log(Level.FINER, "Shutting down RlqsClient with hash {0}", clientHash); From 2f8fe963bfaa1a954bef0339f426b37ac1ecf898 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 15:23:24 -0700 Subject: [PATCH 17/47] Filter chain lifecycle bookmarks - filter provider refactoring TBD --- .../xds/FilterChainMatchingProtocolNegotiators.java | 1 + .../io/grpc/xds/FilterChainSelectorManager.java | 1 + xds/src/main/java/io/grpc/xds/RlqsFilter.java | 2 ++ xds/src/main/java/io/grpc/xds/XdsNameResolver.java | 4 ++++ xds/src/main/java/io/grpc/xds/XdsServerWrapper.java | 13 +++++++++++++ .../java/io/grpc/xds/internal/rlqs/RlqsClient.java | 1 - 6 files changed, 21 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java index 37a0e6a8ae0..087b156c307 100644 --- a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java @@ -108,6 +108,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc drainGraceTime = drainGraceNanosObj; drainGraceTimeUnit = TimeUnit.NANOSECONDS; } + // TODO(sergiitk): [design] drains connections on LDS update. FilterChainSelectorManager.Closer closer = new FilterChainSelectorManager.Closer( new GracefullyShutdownChannelRunnable(ctx.channel(), drainGraceTime, drainGraceTimeUnit)); FilterChainSelector selector = filterChainSelectorManager.register(closer); diff --git a/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java b/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java index 4295d75f59b..f712fe642b7 100644 --- a/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java +++ b/xds/src/main/java/io/grpc/xds/FilterChainSelectorManager.java @@ -65,6 +65,7 @@ public void updateSelector(FilterChainSelector newSelector) { closers = new TreeSet(closers.comparator()); selector = newSelector; } + // TODO(sergiitk): [design] calls the closer of FilterChainMatchingNegotiatorServerFactory for (Closer closer : oldClosers) { closer.closer.run(); } diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 2d3d68a353d..cd99f83e2c9 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -48,6 +48,8 @@ import javax.annotation.Nullable; /** RBAC Http filter implementation. */ +// TODO(sergiitk): introduce a layer between the filter and interceptor. +// lds has filter names and the names are unique - even for server instances. final class RlqsFilter implements Filter, ServerInterceptorBuilder { // private static final Logger logger = Logger.getLogger(RlqsFilter.class.getName()); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 0beb6dc2483..a85c2ba6e61 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -126,6 +126,7 @@ final class XdsNameResolver extends NameResolver { private final ConfigSelector configSelector = new ConfigSelector(); private final long randomChannelId; + // TODO(sergiitk): [filter provider] routing config private volatile RoutingConfig routingConfig = RoutingConfig.empty; private Listener2 listener; private ObjectPool xdsClientPool; @@ -377,6 +378,9 @@ static boolean matchHostName(String hostName, String pattern) { private final class ConfigSelector extends InternalConfigSelector { @Override public Result selectConfig(PickSubchannelArgs args) { + // TODO(sergiitk): [filter provider] config selector, but for the client. + // similar to server's interceptor + // determines what route is used. String cluster = null; Route selectedRoute = null; RoutingConfig routingCfg; diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 108041f95bd..7f2c4e422c8 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -470,13 +470,20 @@ private void updateSelector() { for (FilterChain filterChain: filterChains) { filterChainRouting.put(filterChain, generateRoutingConfig(filterChain)); } + // created new interceptors + FilterChainSelector selector = new FilterChainSelector( Collections.unmodifiableMap(filterChainRouting), defaultFilterChain == null ? null : defaultFilterChain.sslContextProviderSupplier(), defaultFilterChain == null ? new AtomicReference() : generateRoutingConfig(defaultFilterChain)); + + List toRelease = getSuppliersInUse(); + logger.log(Level.FINEST, "Updating selector {0}", selector); + // TODO(sergiitk): [design] implements filter chain replacement logic + filterChainSelectorManager.updateSelector(selector); for (SslContextProviderSupplier e: toRelease) { e.close(); @@ -713,6 +720,7 @@ private void updateRdsRoutingConfig() { // list atomic ref. private void maybeUpdateSelector() { isPending = false; + // TODO(sergiitk): [design] ensures RDS does not cycle connections (see updateSelector); boolean isLastPending = pendingRds.remove(resourceName) && pendingRds.isEmpty(); if (isLastPending) { updateSelector(); @@ -722,7 +730,9 @@ private void maybeUpdateSelector() { } @VisibleForTesting + // TODO(sergiitk): for the life of the server final class ConfigApplyingInterceptor implements ServerInterceptor { + private final ServerInterceptor noopInterceptor = new ServerInterceptor() { @Override public Listener interceptCall(ServerCall call, @@ -734,6 +744,7 @@ public Listener interceptCall(ServerCall call, @Override public Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + // TODO(sergiitk): [design] internal server interceptor: RPC matching logic. AtomicReference routingConfigRef = call.getAttributes().get(ATTR_SERVER_ROUTING_CONFIG); ServerRoutingConfig routingConfig = routingConfigRef == null ? null : @@ -791,6 +802,8 @@ abstract static class ServerRoutingConfig { abstract ImmutableList virtualHosts(); + // TODO(sergiitk): [design] contains per-route interceptor generated by buildserverinterceptopr + // Prebuilt per route server interceptors from http filter configs. abstract ImmutableMap interceptors(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 1101d48515a..5e62cd2d813 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -49,7 +49,6 @@ public RateLimitResult evaluate(HttpMatchInput input) { } bucket = new RlqsBucket(bucketId, bucketSettings); RateLimitResult rateLimitResult = rlqsApiClient.processFirstBucketRequest(bucket); - // TODO(sergiitk): register tickers registerTimers(bucket, bucketSettings); return rateLimitResult; } From fabefe09e48dabb59fb97d03c111ff5b5d3815ec Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 18:04:54 -0700 Subject: [PATCH 18/47] Draft reports and timers --- .../grpc/xds/internal/rlqs/RlqsApiClient.java | 5 +-- .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 7 +++- .../xds/internal/rlqs/RlqsBucketCache.java | 26 ++++++++++++-- .../xds/internal/rlqs/RlqsBucketSettings.java | 5 +-- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 34 +++++++++++++++++-- .../xds/internal/rlqs/RlqsClientPool.java | 8 +++-- 6 files changed, 72 insertions(+), 13 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java index c646c42ea44..d56109cc147 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java @@ -31,6 +31,7 @@ import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -65,9 +66,9 @@ RateLimitResult processFirstBucketRequest(RlqsBucket bucket) { return rateLimitResult; } - void sendUsageReports() { + void sendUsageReports(List buckets) { RateLimitQuotaUsageReports.Builder reports = RateLimitQuotaUsageReports.newBuilder(); - for (RlqsBucket bucket : bucketCache.getBucketsToReport()) { + for (RlqsBucket bucket : buckets) { BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); bucket.reset(); reports.addBucketQuotaUsages(bucketQuotaUsage); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 4f15b1799c8..1277f0aeb8d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -25,11 +25,12 @@ public enum RateLimitResult { // TODO(sergiitk): consider immutable report structure private long numRequestsAllowed = 0; private long numRequestsDenied = 0; - // last_report_time + private long reportingIntervalMillis; // last_assignment_time RlqsBucket(RlqsBucketId bucketId, RlqsBucketSettings bucketSettings) { this.bucketId = bucketId; + this.reportingIntervalMillis = bucketSettings.reportingIntervalMillis(); } RateLimitResult rateLimit() { @@ -54,4 +55,8 @@ public long getNumRequestsDenied() { public long getNumRequestsAllowed() { return numRequestsAllowed; } + + public long getReportingIntervalMillis() { + return reportingIntervalMillis; + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java index 21b7f970160..c98d03a2fba 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java @@ -18,21 +18,41 @@ import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; final class RlqsBucketCache { + // TODO(sergiitk): consider volatile + synchronize instead + private final ConcurrentMap> bucketsPerInterval = new ConcurrentHashMap<>(); + private final ConcurrentMap buckets = new ConcurrentHashMap<>(); RlqsBucket getBucket(RlqsBucketId bucketId) { - return null; + return buckets.get(bucketId); } void insertBucket(RlqsBucket bucket) { + long interval = bucket.getReportingIntervalMillis(); + if (!bucketsPerInterval.containsKey(interval)) { + bucketsPerInterval.put(interval, Sets.newConcurrentHashSet()); + } + bucketsPerInterval.get(bucket.getReportingIntervalMillis()).add(bucket); + buckets.put(bucket.getBucketId(), bucket); } void deleteBucket(RlqsBucketId bucketId) { + RlqsBucket bucket = buckets.get(bucketId); + bucketsPerInterval.get(bucket.getReportingIntervalMillis()).remove(bucket); + buckets.remove(bucket.getBucketId()); } - public ImmutableList getBucketsToReport() { - return ImmutableList.of(); + public ImmutableList getBucketsToReport(long reportingIntervalMillis) { + ImmutableList.Builder report = ImmutableList.builder(); + for (RlqsBucket bucket : bucketsPerInterval.get(reportingIntervalMillis)) { + report.add(bucket); + } + return report.build(); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index f49d2737900..0bdea0ebee2 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -20,6 +20,7 @@ import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; +import com.google.protobuf.util.Durations; import io.grpc.xds.internal.matchers.HttpMatchInput; @AutoValue @@ -31,11 +32,11 @@ public RlqsBucketId toBucketId(HttpMatchInput input) { return null; } - public abstract Duration reportingInterval(); + public abstract long reportingIntervalMillis(); public static RlqsBucketSettings create( ImmutableMap> bucketIdBuilder, Duration reportingInterval) { - return new AutoValue_RlqsBucketSettings(bucketIdBuilder, reportingInterval); + return new AutoValue_RlqsBucketSettings(bucketIdBuilder, Durations.toMillis(reportingInterval)); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 5e62cd2d813..bff425a2191 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -16,10 +16,15 @@ package io.grpc.xds.internal.rlqs; +import com.google.common.collect.ImmutableList; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,12 +35,16 @@ public class RlqsClient { private final Matcher bucketMatchers; private final RlqsBucketCache bucketCache; private final String clientHash; + private final ScheduledExecutorService timeService; + private final ConcurrentHashMap> timers = new ConcurrentHashMap<>(); public RlqsClient( RemoteServerInfo rlqsServer, String domain, - Matcher bucketMatchers, String clientHash) { + Matcher bucketMatchers, String clientHash, + ScheduledExecutorService timeService) { this.bucketMatchers = bucketMatchers; this.clientHash = clientHash; + this.timeService = timeService; bucketCache = new RlqsBucketCache(); rlqsApiClient = new RlqsApiClient(rlqsServer, domain, bucketCache); } @@ -49,14 +58,33 @@ public RateLimitResult evaluate(HttpMatchInput input) { } bucket = new RlqsBucket(bucketId, bucketSettings); RateLimitResult rateLimitResult = rlqsApiClient.processFirstBucketRequest(bucket); - registerTimers(bucket, bucketSettings); + registerReportTimer(bucketSettings.reportingIntervalMillis()); return rateLimitResult; } - private void registerTimers(RlqsBucket bucket, RlqsBucketSettings bucketSettings) { + private void registerReportTimer(final long reportingIntervalMillis) { + // TODO(sergiitk): [IMPL] cap the interval. + if (timers.containsKey(reportingIntervalMillis)) { + return; + } + // TODO(sergiitk): [IMPL] consider manually extending. + ScheduledFuture schedule = timeService.scheduleWithFixedDelay( + () -> reportBucketsWithInterval(reportingIntervalMillis), + reportingIntervalMillis, + reportingIntervalMillis, + TimeUnit.MILLISECONDS); + timers.put(reportingIntervalMillis, schedule); + } + + private void reportBucketsWithInterval(long reportingIntervalMillis) { + ImmutableList bucketsToReport = + bucketCache.getBucketsToReport(reportingIntervalMillis); + // TODO(sergiitk): [IMPL] destroy timer if empty + rlqsApiClient.sendUsageReports(bucketsToReport); } public void shutdown() { + // TODO(sergiitk): [IMPL] Timers shutdown // TODO(sergiitk): [IMPL] RlqsClient shutdown logger.log(Level.FINER, "Shutting down RlqsClient with hash {0}", clientHash); rlqsApiClient.shutdown(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index 92997276487..09c3011136e 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -105,8 +105,12 @@ public RlqsClient getOrCreateRlqsClient(RlqsFilterConfig config) { // TODO(sergiitk): [IMPL] get from bootstrap. RemoteServerInfo rlqsServer = RemoteServerInfo.create(config.rlqsService().targetUri(), InsecureChannelCredentials.create()); - RlqsClient rlqsClient = - new RlqsClient(rlqsServer, config.domain(), config.bucketMatchers(), clientHash); + RlqsClient rlqsClient = new RlqsClient( + rlqsServer, + config.domain(), + config.bucketMatchers(), + clientHash, + timeService); clientPool.put(clientHash, rlqsClient); future.set(clientPool.get(clientHash)); From 027313296db4202bef6fa5254de656c33892cb38 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 18:42:18 -0700 Subject: [PATCH 19/47] RlqsClient -> RlqsEngine --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 8 ++-- .../xds/internal/rlqs/RlqsClientPool.java | 41 ++++++++++--------- .../rlqs/{RlqsClient.java => RlqsEngine.java} | 16 ++++---- 3 files changed, 33 insertions(+), 32 deletions(-) rename xds/src/main/java/io/grpc/xds/internal/rlqs/{RlqsClient.java => RlqsEngine.java} (90%) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index cd99f83e2c9..630dd3bf31e 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -41,8 +41,8 @@ import io.grpc.xds.internal.matchers.OnMatch; import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; -import io.grpc.xds.internal.rlqs.RlqsClient; import io.grpc.xds.internal.rlqs.RlqsClientPool; +import io.grpc.xds.internal.rlqs.RlqsEngine; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; @@ -150,14 +150,14 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { // Being shut down, return no interceptor. return null; } - final RlqsClient rlqsClient = rlqsClientPool.getOrCreateRlqsClient(config); + final RlqsEngine rlqsEngine = rlqsClientPool.getOrCreateRlqsEngine(config); return new ServerInterceptor() { @Override public Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { // Notes: - // map domain() -> an incarnation of bucket matchers, f.e. new RlqsClient(domain, matchers). + // map domain() -> an incarnation of bucket matchers, f.e. new RlqsEngine(domain, matchers). // shared resource holder, acquire every rpc // Store RLQS Client or channel in the config as a reference - FilterConfig config ref // when parse. @@ -174,7 +174,7 @@ public Listener interceptCall( // AI: follow up with Eric on how cache is shared, this changes if we need to cache // interceptor // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. - RateLimitResult result = rlqsClient.evaluate(HttpMatchInput.create(headers, call)); + RateLimitResult result = rlqsEngine.evaluate(HttpMatchInput.create(headers, call)); if (RateLimitResult.ALLOWED.equals(result)) { return next.startCall(call, headers); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index 09c3011136e..d3a8cea48a1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -47,8 +47,8 @@ public final class RlqsClientPool { throw new RlqsPoolSynchronizationException(message, error); }); - private final ConcurrentHashMap clientPool = new ConcurrentHashMap<>(); - Set clientsToShutdown = Sets.newConcurrentHashSet(); + private final ConcurrentHashMap enginePool = new ConcurrentHashMap<>(); + Set enginesToShutdown = Sets.newConcurrentHashSet(); private final ScheduledExecutorService timeService; private final int cleanupIntervalSeconds; @@ -72,11 +72,11 @@ public void run() { if (shutdown) { return; } - for (String clientHash : clientsToShutdown) { - clientPool.get(clientHash).shutdown(); - clientPool.remove(clientHash); + for (String configHash : enginesToShutdown) { + enginePool.get(configHash).shutdown(); + enginePool.remove(configHash); } - clientsToShutdown.clear(); + enginesToShutdown.clear(); }; syncContext.schedule(cleanupTask, cleanupIntervalSeconds, TimeUnit.SECONDS, timeService); } @@ -85,35 +85,35 @@ public void shutdown() { syncContext.execute(() -> { shutdown = true; logger.log(Level.FINER, "Shutting down RlqsClientPool"); - clientsToShutdown.clear(); - for (String clientHash : clientPool.keySet()) { - clientPool.get(clientHash).shutdown(); + enginesToShutdown.clear(); + for (String configHash : enginePool.keySet()) { + enginePool.get(configHash).shutdown(); } - clientPool.clear(); + enginePool.clear(); }); } - public RlqsClient getOrCreateRlqsClient(RlqsFilterConfig config) { - final SettableFuture future = SettableFuture.create(); - final String clientHash = makeRlqsClientHash(config); + public RlqsEngine getOrCreateRlqsEngine(RlqsFilterConfig config) { + final SettableFuture future = SettableFuture.create(); + final String configHash = hashRlqsFilterConfig(config); syncContext.execute(() -> { - if (clientPool.containsKey(clientHash)) { - future.set(clientPool.get(clientHash)); + if (enginePool.containsKey(configHash)) { + future.set(enginePool.get(configHash)); return; } // TODO(sergiitk): [IMPL] get from bootstrap. RemoteServerInfo rlqsServer = RemoteServerInfo.create(config.rlqsService().targetUri(), InsecureChannelCredentials.create()); - RlqsClient rlqsClient = new RlqsClient( + RlqsEngine rlqsEngine = new RlqsEngine( rlqsServer, config.domain(), config.bucketMatchers(), - clientHash, + configHash, timeService); - clientPool.put(clientHash, rlqsClient); - future.set(clientPool.get(clientHash)); + enginePool.put(configHash, rlqsEngine); + future.set(enginePool.get(configHash)); }); try { // TODO(sergiitk): [IMPL] clarify time @@ -124,7 +124,8 @@ public RlqsClient getOrCreateRlqsClient(RlqsFilterConfig config) { } } - private String makeRlqsClientHash(RlqsFilterConfig config) { + private String hashRlqsFilterConfig(RlqsFilterConfig config) { + // TODO(sergiitk): [QUESTION] better name? - ask Eric. // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? // TODO(sergiitk): [IMPL] Hash buckets return config.rlqsService().targetUri() + config.domain(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java similarity index 90% rename from xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java rename to xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index bff425a2191..5a131d8b4dd 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -28,22 +28,22 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class RlqsClient { - private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); +public class RlqsEngine { + private static final Logger logger = Logger.getLogger(RlqsEngine.class.getName()); private final RlqsApiClient rlqsApiClient; private final Matcher bucketMatchers; private final RlqsBucketCache bucketCache; - private final String clientHash; + private final String configHash; private final ScheduledExecutorService timeService; private final ConcurrentHashMap> timers = new ConcurrentHashMap<>(); - public RlqsClient( + public RlqsEngine( RemoteServerInfo rlqsServer, String domain, - Matcher bucketMatchers, String clientHash, + Matcher bucketMatchers, String configHash, ScheduledExecutorService timeService) { this.bucketMatchers = bucketMatchers; - this.clientHash = clientHash; + this.configHash = configHash; this.timeService = timeService; bucketCache = new RlqsBucketCache(); rlqsApiClient = new RlqsApiClient(rlqsServer, domain, bucketCache); @@ -85,8 +85,8 @@ private void reportBucketsWithInterval(long reportingIntervalMillis) { public void shutdown() { // TODO(sergiitk): [IMPL] Timers shutdown - // TODO(sergiitk): [IMPL] RlqsClient shutdown - logger.log(Level.FINER, "Shutting down RlqsClient with hash {0}", clientHash); + // TODO(sergiitk): [IMPL] RlqsEngine shutdown + logger.log(Level.FINER, "Shutting down RlqsEngine with hash {0}", configHash); rlqsApiClient.shutdown(); } } From 9a05cb8a15f2d745b8753099d1b318d99d58f2f5 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 18:51:06 -0700 Subject: [PATCH 20/47] Remove periodic cleanup logic from RlqsClientPool --- .../xds/internal/rlqs/RlqsClientPool.java | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java index d3a8cea48a1..8066f552a7d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java @@ -16,7 +16,6 @@ package io.grpc.xds.internal.rlqs; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.Sets; @@ -37,10 +36,8 @@ public final class RlqsClientPool { private static final Logger logger = Logger.getLogger(RlqsClientPool.class.getName()); - private static final int DEFAULT_CLEANUP_INTERVAL_SECONDS = 10; - // TODO(sergiitk): [QUESTION] always in sync context? - private boolean shutdown; + private volatile boolean shutdown = false; private final SynchronizationContext syncContext = new SynchronizationContext((thread, error) -> { String message = "Uncaught exception in RlqsClientPool SynchronizationContext. Panic!"; logger.log(Level.FINE, message, error); @@ -49,39 +46,24 @@ public final class RlqsClientPool { private final ConcurrentHashMap enginePool = new ConcurrentHashMap<>(); Set enginesToShutdown = Sets.newConcurrentHashSet(); - private final ScheduledExecutorService timeService; - private final int cleanupIntervalSeconds; + private final ScheduledExecutorService scheduler; - private RlqsClientPool(ScheduledExecutorService scheduler, int cleanupIntervalSeconds) { - this.timeService = checkNotNull(scheduler, "scheduler"); - checkArgument(cleanupIntervalSeconds >= 0, "cleanupIntervalSeconds < 0"); - this.cleanupIntervalSeconds = - cleanupIntervalSeconds > 0 ? cleanupIntervalSeconds : DEFAULT_CLEANUP_INTERVAL_SECONDS; + private RlqsClientPool(ScheduledExecutorService scheduler) { + this.scheduler = checkNotNull(scheduler, "scheduler"); } /** Creates an instance. */ public static RlqsClientPool newInstance(ScheduledExecutorService scheduler) { // TODO(sergiitk): [IMPL] scheduler - consider using GrpcUtil.TIMER_SERVICE. // TODO(sergiitk): [IMPL] note that the scheduler has a finite lifetime. - return new RlqsClientPool(scheduler, 0); - } - - public void run() { - Runnable cleanupTask = () -> { - if (shutdown) { - return; - } - for (String configHash : enginesToShutdown) { - enginePool.get(configHash).shutdown(); - enginePool.remove(configHash); - } - enginesToShutdown.clear(); - }; - syncContext.schedule(cleanupTask, cleanupIntervalSeconds, TimeUnit.SECONDS, timeService); + return new RlqsClientPool(scheduler); } public void shutdown() { + if (shutdown) { + return; + } syncContext.execute(() -> { shutdown = true; logger.log(Level.FINER, "Shutting down RlqsClientPool"); @@ -90,6 +72,7 @@ public void shutdown() { enginePool.get(configHash).shutdown(); } enginePool.clear(); + shutdown = false; }); } @@ -110,7 +93,7 @@ public RlqsEngine getOrCreateRlqsEngine(RlqsFilterConfig config) { config.domain(), config.bucketMatchers(), configHash, - timeService); + scheduler); enginePool.put(configHash, rlqsEngine); future.set(enginePool.get(configHash)); From f9758a47a824b75bca3aa9c823a699ae0b9b9df4 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 19:00:33 -0700 Subject: [PATCH 21/47] RlqsClientPool -> RlqsCache --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 22 ++++++++----------- .../{RlqsClientPool.java => RlqsCache.java} | 14 ++++++------ 2 files changed, 16 insertions(+), 20 deletions(-) rename xds/src/main/java/io/grpc/xds/internal/rlqs/{RlqsClientPool.java => RlqsCache.java} (89%) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 630dd3bf31e..89cd4af64b5 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -41,7 +41,7 @@ import io.grpc.xds.internal.matchers.OnMatch; import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; -import io.grpc.xds.internal.rlqs.RlqsClientPool; +import io.grpc.xds.internal.rlqs.RlqsCache; import io.grpc.xds.internal.rlqs.RlqsEngine; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -60,11 +60,7 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { static final String TYPE_URL_OVERRIDE_CONFIG = "type.googleapis.com/" + "envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride"; - private final AtomicReference rlqsClientPoolRef = new AtomicReference<>(); - - // RlqsFilter() { - // rlqsClientPool = new RlqsClientPool() - // } + private final AtomicReference rlqsCache = new AtomicReference<>(); @Override public String[] typeUrls() { @@ -127,7 +123,7 @@ public ServerInterceptor buildServerInterceptor( rlqsFilterConfig = overrideBuilder.build(); } - rlqsClientPoolRef.compareAndSet(null, RlqsClientPool.newInstance(scheduler)); + rlqsCache.compareAndSet(null, RlqsCache.newInstance(scheduler)); return generateRlqsInterceptor(rlqsFilterConfig); } @@ -135,9 +131,9 @@ public ServerInterceptor buildServerInterceptor( public void shutdown() { // TODO(sergiitk): [DESIGN] besides shutting down everything, should there // be per-route interceptor destructors? - RlqsClientPool oldClientPool = rlqsClientPoolRef.getAndUpdate(unused -> null); - if (oldClientPool != null) { - oldClientPool.shutdown(); + RlqsCache oldCache = rlqsCache.getAndUpdate(unused -> null); + if (oldCache != null) { + oldCache.shutdown(); } } @@ -145,12 +141,12 @@ public void shutdown() { private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { checkNotNull(config, "config"); checkNotNull(config.rlqsService(), "config.rlqsService"); - RlqsClientPool rlqsClientPool = rlqsClientPoolRef.get(); - if (rlqsClientPool == null) { + RlqsCache rlqsCache = this.rlqsCache.get(); + if (rlqsCache == null) { // Being shut down, return no interceptor. return null; } - final RlqsEngine rlqsEngine = rlqsClientPool.getOrCreateRlqsEngine(config); + final RlqsEngine rlqsEngine = rlqsCache.getOrCreateRlqsEngine(config); return new ServerInterceptor() { @Override diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java similarity index 89% rename from xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java rename to xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 8066f552a7d..15963d4ddfe 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClientPool.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -33,13 +33,13 @@ import java.util.logging.Level; import java.util.logging.Logger; -public final class RlqsClientPool { - private static final Logger logger = Logger.getLogger(RlqsClientPool.class.getName()); +public final class RlqsCache { + private static final Logger logger = Logger.getLogger(RlqsCache.class.getName()); // TODO(sergiitk): [QUESTION] always in sync context? private volatile boolean shutdown = false; private final SynchronizationContext syncContext = new SynchronizationContext((thread, error) -> { - String message = "Uncaught exception in RlqsClientPool SynchronizationContext. Panic!"; + String message = "Uncaught exception in RlqsCache SynchronizationContext. Panic!"; logger.log(Level.FINE, message, error); throw new RlqsPoolSynchronizationException(message, error); }); @@ -49,15 +49,15 @@ public final class RlqsClientPool { private final ScheduledExecutorService scheduler; - private RlqsClientPool(ScheduledExecutorService scheduler) { + private RlqsCache(ScheduledExecutorService scheduler) { this.scheduler = checkNotNull(scheduler, "scheduler"); } /** Creates an instance. */ - public static RlqsClientPool newInstance(ScheduledExecutorService scheduler) { + public static RlqsCache newInstance(ScheduledExecutorService scheduler) { // TODO(sergiitk): [IMPL] scheduler - consider using GrpcUtil.TIMER_SERVICE. // TODO(sergiitk): [IMPL] note that the scheduler has a finite lifetime. - return new RlqsClientPool(scheduler); + return new RlqsCache(scheduler); } public void shutdown() { @@ -66,7 +66,7 @@ public void shutdown() { } syncContext.execute(() -> { shutdown = true; - logger.log(Level.FINER, "Shutting down RlqsClientPool"); + logger.log(Level.FINER, "Shutting down RlqsCache"); enginesToShutdown.clear(); for (String configHash : enginePool.keySet()) { enginePool.get(configHash).shutdown(); From e1f289eec29ff9f5fee46f9ac8358887330e7599 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 24 Sep 2024 19:43:00 -0700 Subject: [PATCH 22/47] RlqsApiClient -> RlqsClient --- .../{RlqsApiClient.java => RlqsClient.java} | 35 ++++++++++++------- .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 10 +++--- 2 files changed, 27 insertions(+), 18 deletions(-) rename xds/src/main/java/io/grpc/xds/internal/rlqs/{RlqsApiClient.java => RlqsClient.java} (80%) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java similarity index 80% rename from xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java rename to xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index d56109cc147..61716cce6a2 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsApiClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -33,21 +33,22 @@ import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; -public final class RlqsApiClient { - private static final Logger logger = Logger.getLogger(RlqsApiClient.class.getName()); +public final class RlqsClient { + private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); private final RemoteServerInfo serverInfo; private final String domain; - private final RlqsApiClientInternal rlqsApiClient; + private final RlqsStream rlqsStream; private final RlqsBucketCache bucketCache; - RlqsApiClient(RemoteServerInfo serverInfo, String domain, RlqsBucketCache bucketCache) { + RlqsClient(RemoteServerInfo serverInfo, String domain, RlqsBucketCache bucketCache) { this.serverInfo = serverInfo; this.domain = domain; - this.rlqsApiClient = new RlqsApiClientInternal(serverInfo); + this.rlqsStream = new RlqsStream(serverInfo, domain); this.bucketCache = bucketCache; } @@ -58,8 +59,9 @@ RateLimitResult processFirstBucketRequest(RlqsBucket bucket) { // Send initial usage report. BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); + // TODO(sergiitk): [IMPL] domain logic not needed anymore. bucket.reset(); - rlqsApiClient.reportUsage(RateLimitQuotaUsageReports.newBuilder() + rlqsStream.reportUsage(RateLimitQuotaUsageReports.newBuilder() .setDomain(domain) .addBucketQuotaUsages(bucketQuotaUsage) .build()); @@ -73,7 +75,7 @@ void sendUsageReports(List buckets) { bucket.reset(); reports.addBucketQuotaUsages(bucketQuotaUsage); } - rlqsApiClient.reportUsage(reports.build()); + rlqsStream.reportUsage(reports.build()); } void abandonBucket(RlqsBucketId bucketId) { @@ -91,29 +93,36 @@ BucketQuotaUsage toUsageReport(RlqsBucket bucket) { } public void shutdown() { - logger.log(Level.FINER, "Shutting down RlqsApiClient to {0}", serverInfo.target()); - // TODO(sergiitk): [IMPL] RlqsApiClient shutdown + logger.log(Level.FINER, "Shutting down RlqsClient to {0}", serverInfo.target()); + // TODO(sergiitk): [IMPL] RlqsClient shutdown } - private class RlqsApiClientInternal { + private class RlqsStream { + private final AtomicBoolean isFirstReport = new AtomicBoolean(true); private final ManagedChannel channel; - private final RateLimitQuotaServiceStub stub; + private final String domain; private final ClientCallStreamObserver clientCallStream; - RlqsApiClientInternal(RemoteServerInfo serverInfo) { + RlqsStream(RemoteServerInfo serverInfo, String domain) { + this.domain = domain; channel = Grpc.newChannelBuilder(serverInfo.target(), serverInfo.channelCredentials()) .keepAliveTime(10, TimeUnit.SECONDS) .keepAliveWithoutCalls(true) .build(); // keepalive? // TODO(sergiitk): [IMPL] Manage State changes? - stub = RateLimitQuotaServiceGrpc.newStub(channel); + RateLimitQuotaServiceStub stub = RateLimitQuotaServiceGrpc.newStub(channel); clientCallStream = (ClientCallStreamObserver) stub.streamRateLimitQuotas(new RlqsStreamObserver()); // TODO(sergiitk): [IMPL] set on ready handler? + // TODO(sergiitk): [QUESTION] a nice way to handle setting domain in the first usage report? + // - probably an interceptor } void reportUsage(RateLimitQuotaUsageReports usageReports) { + if (isFirstReport.compareAndSet(true, false)) { + usageReports = usageReports.toBuilder().setDomain(domain).build(); + } clientCallStream.onNext(usageReports); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index 5a131d8b4dd..e179dba60f0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -31,7 +31,7 @@ public class RlqsEngine { private static final Logger logger = Logger.getLogger(RlqsEngine.class.getName()); - private final RlqsApiClient rlqsApiClient; + private final RlqsClient rlqsClient; private final Matcher bucketMatchers; private final RlqsBucketCache bucketCache; private final String configHash; @@ -46,7 +46,7 @@ public RlqsEngine( this.configHash = configHash; this.timeService = timeService; bucketCache = new RlqsBucketCache(); - rlqsApiClient = new RlqsApiClient(rlqsServer, domain, bucketCache); + rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); } public RateLimitResult evaluate(HttpMatchInput input) { @@ -57,7 +57,7 @@ public RateLimitResult evaluate(HttpMatchInput input) { return bucket.rateLimit(); } bucket = new RlqsBucket(bucketId, bucketSettings); - RateLimitResult rateLimitResult = rlqsApiClient.processFirstBucketRequest(bucket); + RateLimitResult rateLimitResult = rlqsClient.processFirstBucketRequest(bucket); registerReportTimer(bucketSettings.reportingIntervalMillis()); return rateLimitResult; } @@ -80,13 +80,13 @@ private void reportBucketsWithInterval(long reportingIntervalMillis) { ImmutableList bucketsToReport = bucketCache.getBucketsToReport(reportingIntervalMillis); // TODO(sergiitk): [IMPL] destroy timer if empty - rlqsApiClient.sendUsageReports(bucketsToReport); + rlqsClient.sendUsageReports(bucketsToReport); } public void shutdown() { // TODO(sergiitk): [IMPL] Timers shutdown // TODO(sergiitk): [IMPL] RlqsEngine shutdown logger.log(Level.FINER, "Shutting down RlqsEngine with hash {0}", configHash); - rlqsApiClient.shutdown(); + rlqsClient.shutdown(); } } From bf7c410cb1d0915858ce12825c7b3304706ac9ec Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 25 Sep 2024 14:58:05 -0700 Subject: [PATCH 23/47] More class drafting --- xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java | 6 +++++- xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 61716cce6a2..cb449eb4768 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -52,7 +52,7 @@ public final class RlqsClient { this.bucketCache = bucketCache; } - RateLimitResult processFirstBucketRequest(RlqsBucket bucket) { + RateLimitResult sendInitialReport(RlqsBucket bucket) { bucketCache.insertBucket(bucket); // Register first request to the bucket for the initial report. RateLimitResult rateLimitResult = bucket.rateLimit(); @@ -97,6 +97,10 @@ public void shutdown() { // TODO(sergiitk): [IMPL] RlqsClient shutdown } + public void handleStreamClosed() { + // TODO(sergiitk): [IMPL] reconnect on stream down. + } + private class RlqsStream { private final AtomicBoolean isFirstReport = new AtomicBoolean(true); private final ManagedChannel channel; diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index e179dba60f0..104846cd14a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -57,7 +57,7 @@ public RateLimitResult evaluate(HttpMatchInput input) { return bucket.rateLimit(); } bucket = new RlqsBucket(bucketId, bucketSettings); - RateLimitResult rateLimitResult = rlqsClient.processFirstBucketRequest(bucket); + RateLimitResult rateLimitResult = rlqsClient.sendInitialReport(bucket); registerReportTimer(bucketSettings.reportingIntervalMillis()); return rateLimitResult; } From 92169c64ae6bcaad8897854a8bfce5b21a2e72b9 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 25 Sep 2024 18:08:16 -0700 Subject: [PATCH 24/47] Create proper RateLimitResult --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 9 ++- .../xds/internal/rlqs/RateLimitResult.java | 67 +++++++++++++++++++ .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 6 +- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 1 - .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 1 - 5 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 89cd4af64b5..b2df189ce43 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -32,14 +32,13 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; -import io.grpc.Status; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.matchers.MatcherList; import io.grpc.xds.internal.matchers.OnMatch; -import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; +import io.grpc.xds.internal.rlqs.RateLimitResult; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import io.grpc.xds.internal.rlqs.RlqsCache; import io.grpc.xds.internal.rlqs.RlqsEngine; @@ -171,11 +170,11 @@ public Listener interceptCall( // interceptor // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. RateLimitResult result = rlqsEngine.evaluate(HttpMatchInput.create(headers, call)); - if (RateLimitResult.ALLOWED.equals(result)) { + if (result.isAllowed()) { return next.startCall(call, headers); } - Status status = Status.UNAVAILABLE.withDescription(""); - call.close(status, new Metadata()); + RateLimitResult.DenyResponse denyResponse = result.denyResponse().get(); + call.close(denyResponse.status(), denyResponse.headersToAdd()); return new ServerCall.Listener(){}; } }; diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java new file mode 100644 index 00000000000..482c72fbe92 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import com.google.auto.value.AutoValue; +import io.grpc.Metadata; +import io.grpc.Status; +import java.util.Optional; +import javax.annotation.Nullable; + +@AutoValue +public abstract class RateLimitResult { + + public abstract Optional denyResponse(); + + public final boolean isAllowed() { + return !isDenied(); + } + + public final boolean isDenied() { + return denyResponse().isPresent(); + } + + public static RateLimitResult deny(@Nullable DenyResponse denyResponse) { + if (denyResponse == null) { + denyResponse = DenyResponse.create(); + } + return new AutoValue_RateLimitResult(Optional.of(denyResponse)); + } + + public static RateLimitResult allow() { + return new AutoValue_RateLimitResult(Optional.empty()); + } + + @AutoValue + public abstract static class DenyResponse { + public abstract Status status(); + + public abstract Metadata headersToAdd(); + + public static DenyResponse create(Status status, Metadata headersToAdd) { + return new AutoValue_RateLimitResult_DenyResponse(status, headersToAdd); + } + + public static DenyResponse create(Status status) { + return create(status, new Metadata()); + } + + public static DenyResponse create() { + return create(Status.UNAVAILABLE.withDescription("")); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 1277f0aeb8d..50e386c0f06 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -17,10 +17,6 @@ package io.grpc.xds.internal.rlqs; public class RlqsBucket { - public enum RateLimitResult { - ALLOWED, DENIED - } - private final RlqsBucketId bucketId; // TODO(sergiitk): consider immutable report structure private long numRequestsAllowed = 0; @@ -36,7 +32,7 @@ public enum RateLimitResult { RateLimitResult rateLimit() { // TODO(sergiitk): impl numRequestsAllowed += 1; - return RateLimitResult.ALLOWED; + return RateLimitResult.allow(); } void reset() { diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index cb449eb4768..9296fec9c9c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -30,7 +30,6 @@ import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; -import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index 104846cd14a..f0f65028b66 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -20,7 +20,6 @@ import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; -import io.grpc.xds.internal.rlqs.RlqsBucket.RateLimitResult; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; From 725d7b8fd80045a4034a730ebd6e44d5bb7bd10a Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 26 Sep 2024 16:43:05 -0700 Subject: [PATCH 25/47] Draft Bucket: Usage Reports, RateLimitStrategy, TTLs --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 1 + .../internal/datatype/RateLimitStrategy.java | 78 ++++++++++++ .../xds/internal/rlqs/RateLimitResult.java | 2 + .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 117 ++++++++++++++---- .../xds/internal/rlqs/RlqsBucketCache.java | 22 +++- .../grpc/xds/internal/rlqs/RlqsBucketId.java | 11 +- .../xds/internal/rlqs/RlqsBucketSettings.java | 9 ++ .../io/grpc/xds/internal/rlqs/RlqsCache.java | 15 ++- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 57 ++++----- 9 files changed, 248 insertions(+), 64 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index b2df189ce43..6a5c7b6e222 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -145,6 +145,7 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { // Being shut down, return no interceptor. return null; } + final RlqsEngine rlqsEngine = rlqsCache.getOrCreateRlqsEngine(config); return new ServerInterceptor() { diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java new file mode 100644 index 00000000000..32d4a3ea865 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.datatype; + +import com.google.auto.value.AutoOneOf; +import io.envoyproxy.envoy.type.v3.RateLimitStrategy.BlanketRule; +import io.envoyproxy.envoy.type.v3.TokenBucket; +import io.grpc.xds.internal.rlqs.RateLimitResult; + +@AutoOneOf(RateLimitStrategy.Kind.class) +public abstract class RateLimitStrategy { + // TODO(sergiitk): instead, make RateLimitStrategy interface, + // and AllowAll DenyAll, TokenBucket extending it + public enum Kind { BLANKET_RULE, TOKEN_BUCKET } + + public abstract Kind getKind(); + + public final RateLimitResult rateLimit() { + switch (getKind()) { + case BLANKET_RULE: + switch (blanketRule()) { + case DENY_ALL: + return RateLimitResult.deny(null); + case ALLOW_ALL: + default: + return RateLimitResult.allow(); + } + case TOKEN_BUCKET: + throw new UnsupportedOperationException("Not implemented yet"); + default: + throw new UnsupportedOperationException("Unexpected strategy kind"); + } + } + + // TODO(sergiitk): [IMPL] Replace with the internal class. + public abstract BlanketRule blanketRule(); + + // TODO(sergiitk): [IMPL] Replace with the implementation class. + public abstract TokenBucket tokenBucket(); + + public static RateLimitStrategy ofBlanketRule(BlanketRule blanketRuleProto) { + return AutoOneOf_RateLimitStrategy.blanketRule(blanketRuleProto); + } + + public static RateLimitStrategy ofTokenBucket(TokenBucket tokenBucketProto) { + return AutoOneOf_RateLimitStrategy.tokenBucket(tokenBucketProto); + } + + public static RateLimitStrategy fromEnvoyProto( + io.envoyproxy.envoy.type.v3.RateLimitStrategy rateLimitStrategyProto) { + switch (rateLimitStrategyProto.getStrategyCase()) { + case BLANKET_RULE: + return ofBlanketRule(rateLimitStrategyProto.getBlanketRule()); + case TOKEN_BUCKET: + return ofTokenBucket(rateLimitStrategyProto.getTokenBucket()); + case REQUESTS_PER_TIME_UNIT: + // TODO(sergiitk): [IMPL] convert to token bucket; + throw new UnsupportedOperationException("Not implemented yet"); + default: + // TODO(sergiitk): [IMPL[ replace with a custom exception. + throw new UnsupportedOperationException("Unknown RL type"); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java index 482c72fbe92..ccecd2b0707 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java @@ -24,6 +24,8 @@ @AutoValue public abstract class RateLimitResult { + // TODO(sergiitk): make RateLimitResult an interface, + // RlqsRateLimitResult extends it - which contains DenyResponse. public abstract Optional denyResponse(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 50e386c0f06..25227f13518 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -16,43 +16,118 @@ package io.grpc.xds.internal.rlqs; +import com.google.auto.value.AutoValue; +import io.grpc.Deadline; +import io.grpc.xds.internal.datatype.RateLimitStrategy; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; + + public class RlqsBucket { private final RlqsBucketId bucketId; - // TODO(sergiitk): consider immutable report structure - private long numRequestsAllowed = 0; - private long numRequestsDenied = 0; - private long reportingIntervalMillis; - // last_assignment_time + private final long reportingIntervalMillis; + + private final RateLimitStrategy noAssignmentStrategy; + private final RateLimitStrategy expiredAssignmentStrategy; + + // TODO(sergiitk): [impl] consider AtomicLongFieldUpdater + private final AtomicLong lastSnapshotTimeNanos = new AtomicLong(-1); + private final AtomicLong numRequestsAllowed = new AtomicLong(); + private final AtomicLong numRequestsDenied = new AtomicLong(); + + // TODO(sergiitk): [impl] consider AtomicReferenceFieldUpdater + @Nullable + private volatile RateLimitStrategy assignmentStrategy = null; + private volatile long assignmentExpiresTimeNanos; + // TODO(sergiitk): needed for expired_assignment_behavior_timeout + private volatile long lastAssignmentTimeNanos; RlqsBucket(RlqsBucketId bucketId, RlqsBucketSettings bucketSettings) { + // TODO(sergiitk): [design] consider lock per bucket instance this.bucketId = bucketId; - this.reportingIntervalMillis = bucketSettings.reportingIntervalMillis(); + reportingIntervalMillis = bucketSettings.reportingIntervalMillis(); + expiredAssignmentStrategy = bucketSettings.expiredAssignmentStrategy(); + noAssignmentStrategy = bucketSettings.noAssignmentStrategy(); } - RateLimitResult rateLimit() { - // TODO(sergiitk): impl - numRequestsAllowed += 1; - return RateLimitResult.allow(); + public RlqsBucketId getBucketId() { + return bucketId; } - void reset() { - numRequestsAllowed = 0; - numRequestsDenied = 0; + public long getReportingIntervalMillis() { + return reportingIntervalMillis; } - public RlqsBucketId getBucketId() { - return bucketId; + public RateLimitResult rateLimit() { + RateLimitResult rateLimitResult = resolveStrategy().rateLimit(); + if (rateLimitResult.isAllowed()) { + numRequestsAllowed.incrementAndGet(); + } else { + numRequestsDenied.incrementAndGet(); + } + // TODO(sergiitk): [impl] when RateLimitResult broken into RlqsRateLimitResult, + // augment with deny response strategy + return rateLimitResult; + } + + private RateLimitStrategy resolveStrategy() { + if (assignmentStrategy == null) { + return noAssignmentStrategy; + } + if (assignmentExpiresTimeNanos > nanoTimeNow()) { + // TODO(sergiitk): handle expired behavior properly: it has own ttl, + // after the bucket is abandoned. + // Also, there's reuse last assignment option. + return expiredAssignmentStrategy; + } + return assignmentStrategy; } - public long getNumRequestsDenied() { - return numRequestsDenied; + public RlqsBucketUsage snapshotAndResetUsage() { + // TODO(sergiitk): [IMPL] ensure synchronized + long snapAllowed = numRequestsAllowed.get(); + long snapDenied = numRequestsDenied.get(); + long snapTime = nanoTimeNow(); + + // Reset stats. + numRequestsAllowed.addAndGet(-snapAllowed); + numRequestsDenied.addAndGet(-snapDenied); + + long lastSnapTime = lastSnapshotTimeNanos.getAndSet(snapTime); + // First snapshot. + if (lastSnapTime < 0) { + lastSnapTime = snapTime; + } + return RlqsBucketUsage.create(bucketId, snapAllowed, snapDenied, snapTime - lastSnapTime); } - public long getNumRequestsAllowed() { - return numRequestsAllowed; + public void updateAction(RateLimitStrategy strategy, long ttlMillis) { + // TODO(sergiitk): [IMPL] ensure synchronized + lastAssignmentTimeNanos = nanoTimeNow(); + assignmentExpiresTimeNanos = lastAssignmentTimeNanos + (ttlMillis * 1_000_000); + assignmentStrategy = strategy; } - public long getReportingIntervalMillis() { - return reportingIntervalMillis; + private static long nanoTimeNow() { + return Deadline.getSystemTicker().nanoTime(); + } + + @AutoValue + public abstract static class RlqsBucketUsage { + + public abstract RlqsBucketId bucketId(); + + public abstract long numRequestsAllowed(); + + public abstract long numRequestsDenied(); + + public abstract long timeElapsedNanos(); + + public static RlqsBucketUsage create( + RlqsBucketId bucketId, long numRequestsAllowed, long numRequestsDenied, + long timeElapsedNanos) { + return new AutoValue_RlqsBucket_RlqsBucketUsage(bucketId, numRequestsAllowed, + numRequestsDenied, timeElapsedNanos); + } } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java index c98d03a2fba..6983114e7dc 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; +import io.grpc.xds.internal.datatype.RateLimitStrategy; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -33,12 +34,19 @@ RlqsBucket getBucket(RlqsBucketId bucketId) { } void insertBucket(RlqsBucket bucket) { - long interval = bucket.getReportingIntervalMillis(); - if (!bucketsPerInterval.containsKey(interval)) { - bucketsPerInterval.put(interval, Sets.newConcurrentHashSet()); + // read synchronize trick + if (buckets.get(bucket.getBucketId()) != null) { + return; + } + synchronized (this) { + long interval = bucket.getReportingIntervalMillis(); + if (!bucketsPerInterval.containsKey(interval)) { + bucketsPerInterval.put(interval, Sets.newConcurrentHashSet()); + } + + bucketsPerInterval.get(bucket.getReportingIntervalMillis()).add(bucket); + buckets.put(bucket.getBucketId(), bucket); } - bucketsPerInterval.get(bucket.getReportingIntervalMillis()).add(bucket); - buckets.put(bucket.getBucketId(), bucket); } void deleteBucket(RlqsBucketId bucketId) { @@ -47,6 +55,10 @@ void deleteBucket(RlqsBucketId bucketId) { buckets.remove(bucket.getBucketId()); } + void updateBucket(RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { + RlqsBucket bucket = buckets.get(bucketId); + bucket.updateAction(rateLimitStrategy, ttlMillis); + } public ImmutableList getBucketsToReport(long reportingIntervalMillis) { ImmutableList.Builder report = ImmutableList.builder(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java index 15eccfe2d6f..65360ce0e23 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java @@ -17,7 +17,9 @@ package io.grpc.xds.internal.rlqs; import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; import com.google.common.collect.ImmutableMap; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.BucketId; import java.util.Map; @AutoValue @@ -28,13 +30,18 @@ public static RlqsBucketId create(ImmutableMap bucketId) { return new AutoValue_RlqsBucketId(bucketId); } - public static RlqsBucketId fromEnvoyProto( - io.envoyproxy.envoy.service.rate_limit_quota.v3.BucketId envoyProto) { + public static RlqsBucketId fromEnvoyProto(BucketId envoyProto) { ImmutableMap.Builder bucketId = ImmutableMap.builder(); for (Map.Entry entry : envoyProto.getBucketMap().entrySet()) { bucketId.put(entry.getKey(), entry.getValue()); } return RlqsBucketId.create(bucketId.build()); + } + @Memoized + public BucketId toEnvoyProto() { + // TODO(sergiitk): [impl] can be cached. + return BucketId.newBuilder().putAllBucket(bucketId()).build(); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index 0bdea0ebee2..ebc23085770 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; +import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.matchers.HttpMatchInput; @AutoValue @@ -32,6 +33,14 @@ public RlqsBucketId toBucketId(HttpMatchInput input) { return null; } + public RateLimitStrategy noAssignmentStrategy() { + return null; + } + + public RateLimitStrategy expiredAssignmentStrategy() { + return null; + } + public abstract long reportingIntervalMillis(); public static RlqsBucketSettings create( diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 15963d4ddfe..6b020b68a1c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -76,16 +76,19 @@ public void shutdown() { }); } + public void forgetRlqsEngine(RlqsFilterConfig oldConfig) { + // TODO(sergiitk): shutdown one + } + public RlqsEngine getOrCreateRlqsEngine(RlqsFilterConfig config) { - final SettableFuture future = SettableFuture.create(); final String configHash = hashRlqsFilterConfig(config); + if (enginePool.containsKey(configHash)) { + return enginePool.get(configHash); + } + final SettableFuture future = SettableFuture.create(); syncContext.execute(() -> { - if (enginePool.containsKey(configHash)) { - future.set(enginePool.get(configHash)); - return; - } - // TODO(sergiitk): [IMPL] get from bootstrap. + // TODO(sergiitk): [IMPL] get channel creds from the bootstrap. RemoteServerInfo rlqsServer = RemoteServerInfo.create(config.rlqsService().targetUri(), InsecureChannelCredentials.create()); RlqsEngine rlqsEngine = new RlqsEngine( diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 9296fec9c9c..d341f7b36ad 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -16,7 +16,8 @@ package io.grpc.xds.internal.rlqs; -import com.google.protobuf.Duration; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction; @@ -24,12 +25,12 @@ import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaServiceGrpc.RateLimitQuotaServiceStub; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports.BucketQuotaUsage; -import io.envoyproxy.envoy.type.v3.RateLimitStrategy; import io.grpc.Grpc; import io.grpc.ManagedChannel; import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.internal.datatype.RateLimitStrategy; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,39 +41,27 @@ public final class RlqsClient { private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); private final RemoteServerInfo serverInfo; - private final String domain; private final RlqsStream rlqsStream; private final RlqsBucketCache bucketCache; RlqsClient(RemoteServerInfo serverInfo, String domain, RlqsBucketCache bucketCache) { this.serverInfo = serverInfo; - this.domain = domain; this.rlqsStream = new RlqsStream(serverInfo, domain); this.bucketCache = bucketCache; } RateLimitResult sendInitialReport(RlqsBucket bucket) { bucketCache.insertBucket(bucket); - // Register first request to the bucket for the initial report. + // Register the first request to the bucket for the initial report. RateLimitResult rateLimitResult = bucket.rateLimit(); - - // Send initial usage report. - BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); - // TODO(sergiitk): [IMPL] domain logic not needed anymore. - bucket.reset(); - rlqsStream.reportUsage(RateLimitQuotaUsageReports.newBuilder() - .setDomain(domain) - .addBucketQuotaUsages(bucketQuotaUsage) - .build()); + rlqsStream.reportUsage(ImmutableList.of(bucket.snapshotAndResetUsage())); return rateLimitResult; } void sendUsageReports(List buckets) { - RateLimitQuotaUsageReports.Builder reports = RateLimitQuotaUsageReports.newBuilder(); + ImmutableList.Builder reports = ImmutableList.builder(); for (RlqsBucket bucket : buckets) { - BucketQuotaUsage bucketQuotaUsage = toUsageReport(bucket); - bucket.reset(); - reports.addBucketQuotaUsages(bucketQuotaUsage); + reports.add(bucket.snapshotAndResetUsage()); } rlqsStream.reportUsage(reports.build()); } @@ -82,13 +71,8 @@ void abandonBucket(RlqsBucketId bucketId) { } void updateBucketAssignment( - RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, Duration duration) { - // Deadline.after(Durations.toMillis(ttl), TimeUnit.MILLISECONDS); - } - - BucketQuotaUsage toUsageReport(RlqsBucket bucket) { - // TODO(sergiitk): consider moving to RlqsBucket, and adding something like reportAndReset - return null; + RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { + bucketCache.updateBucket(bucketId, rateLimitStrategy, ttlMillis); } public void shutdown() { @@ -122,11 +106,24 @@ private class RlqsStream { // - probably an interceptor } - void reportUsage(RateLimitQuotaUsageReports usageReports) { + private BucketQuotaUsage toUsageReport(RlqsBucket.RlqsBucketUsage usage) { + return BucketQuotaUsage.newBuilder() + .setBucketId(usage.bucketId().toEnvoyProto()) + .setNumRequestsAllowed(usage.numRequestsAllowed()) + .setNumRequestsDenied(usage.numRequestsDenied()) + .setTimeElapsed(Durations.fromNanos(usage.timeElapsedNanos())) + .build(); + } + + void reportUsage(List usageReports) { + RateLimitQuotaUsageReports.Builder report = RateLimitQuotaUsageReports.newBuilder(); if (isFirstReport.compareAndSet(true, false)) { - usageReports = usageReports.toBuilder().setDomain(domain).build(); + report.setDomain(domain); + } + for (RlqsBucket.RlqsBucketUsage bucketUsage : usageReports) { + report.addBucketQuotaUsages(toUsageReport(bucketUsage)); } - clientCallStream.onNext(usageReports); + clientCallStream.onNext(report.build()); } /** @@ -146,8 +143,8 @@ public void onNext(RateLimitQuotaResponse response) { case QUOTA_ASSIGNMENT_ACTION: QuotaAssignmentAction quotaAssignmentAction = bucketAction.getQuotaAssignmentAction(); updateBucketAssignment(RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId()), - quotaAssignmentAction.getRateLimitStrategy(), - quotaAssignmentAction.getAssignmentTimeToLive()); + RateLimitStrategy.fromEnvoyProto(quotaAssignmentAction.getRateLimitStrategy()), + Durations.toMillis(quotaAssignmentAction.getAssignmentTimeToLive())); break; default: // TODO(sergiitk): error From 5588d8bfb78eb44eb398efa185a64c44e9230fad Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 11:18:18 -0700 Subject: [PATCH 26/47] Improve method names --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 2 +- xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java | 2 +- xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 6a5c7b6e222..0b267f275be 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -170,7 +170,7 @@ public Listener interceptCall( // AI: follow up with Eric on how cache is shared, this changes if we need to cache // interceptor // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. - RateLimitResult result = rlqsEngine.evaluate(HttpMatchInput.create(headers, call)); + RateLimitResult result = rlqsEngine.rateLimit(HttpMatchInput.create(headers, call)); if (result.isAllowed()) { return next.startCall(call, headers); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 6b020b68a1c..359a4689fc1 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -76,7 +76,7 @@ public void shutdown() { }); } - public void forgetRlqsEngine(RlqsFilterConfig oldConfig) { + public void shutdownRlqsEngine(RlqsFilterConfig oldConfig) { // TODO(sergiitk): shutdown one } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index f0f65028b66..da85dd8a5b9 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -48,7 +48,7 @@ public RlqsEngine( rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); } - public RateLimitResult evaluate(HttpMatchInput input) { + public RateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); RlqsBucket bucket = bucketCache.getBucket(bucketId); From 04f0d0213b74b5051bf2eddb902ab064e15abbb5 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 11:44:44 -0700 Subject: [PATCH 27/47] RateLimitResult -> RlqsRateLimitResult --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 6 +++--- .../internal/datatype/RateLimitStrategy.java | 7 +++---- .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 17 ++++++++------- .../xds/internal/rlqs/RlqsBucketSettings.java | 6 ++++++ .../io/grpc/xds/internal/rlqs/RlqsClient.java | 6 +++--- .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 6 +++--- ...itResult.java => RlqsRateLimitResult.java} | 21 +++++++++---------- 7 files changed, 37 insertions(+), 32 deletions(-) rename xds/src/main/java/io/grpc/xds/internal/rlqs/{RateLimitResult.java => RlqsRateLimitResult.java} (73%) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 0b267f275be..a49cc31dc44 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -38,10 +38,10 @@ import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.matchers.MatcherList; import io.grpc.xds.internal.matchers.OnMatch; -import io.grpc.xds.internal.rlqs.RateLimitResult; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import io.grpc.xds.internal.rlqs.RlqsCache; import io.grpc.xds.internal.rlqs.RlqsEngine; +import io.grpc.xds.internal.rlqs.RlqsRateLimitResult; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; @@ -170,11 +170,11 @@ public Listener interceptCall( // AI: follow up with Eric on how cache is shared, this changes if we need to cache // interceptor // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. - RateLimitResult result = rlqsEngine.rateLimit(HttpMatchInput.create(headers, call)); + RlqsRateLimitResult result = rlqsEngine.rateLimit(HttpMatchInput.create(headers, call)); if (result.isAllowed()) { return next.startCall(call, headers); } - RateLimitResult.DenyResponse denyResponse = result.denyResponse().get(); + RlqsRateLimitResult.DenyResponse denyResponse = result.denyResponse().get(); call.close(denyResponse.status(), denyResponse.headersToAdd()); return new ServerCall.Listener(){}; } diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java index 32d4a3ea865..8cfca8cb967 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java @@ -19,7 +19,6 @@ import com.google.auto.value.AutoOneOf; import io.envoyproxy.envoy.type.v3.RateLimitStrategy.BlanketRule; import io.envoyproxy.envoy.type.v3.TokenBucket; -import io.grpc.xds.internal.rlqs.RateLimitResult; @AutoOneOf(RateLimitStrategy.Kind.class) public abstract class RateLimitStrategy { @@ -29,15 +28,15 @@ public enum Kind { BLANKET_RULE, TOKEN_BUCKET } public abstract Kind getKind(); - public final RateLimitResult rateLimit() { + public final boolean rateLimit() { switch (getKind()) { case BLANKET_RULE: switch (blanketRule()) { case DENY_ALL: - return RateLimitResult.deny(null); + return true; case ALLOW_ALL: default: - return RateLimitResult.allow(); + return false; } case TOKEN_BUCKET: throw new UnsupportedOperationException("Not implemented yet"); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 25227f13518..10f6469b509 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -19,6 +19,7 @@ import com.google.auto.value.AutoValue; import io.grpc.Deadline; import io.grpc.xds.internal.datatype.RateLimitStrategy; +import io.grpc.xds.internal.rlqs.RlqsRateLimitResult.DenyResponse; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; @@ -34,6 +35,7 @@ public class RlqsBucket { private final AtomicLong lastSnapshotTimeNanos = new AtomicLong(-1); private final AtomicLong numRequestsAllowed = new AtomicLong(); private final AtomicLong numRequestsDenied = new AtomicLong(); + private final DenyResponse denyResponse; // TODO(sergiitk): [impl] consider AtomicReferenceFieldUpdater @Nullable @@ -48,6 +50,7 @@ public class RlqsBucket { reportingIntervalMillis = bucketSettings.reportingIntervalMillis(); expiredAssignmentStrategy = bucketSettings.expiredAssignmentStrategy(); noAssignmentStrategy = bucketSettings.noAssignmentStrategy(); + denyResponse = bucketSettings.denyResponse(); } public RlqsBucketId getBucketId() { @@ -58,16 +61,14 @@ public long getReportingIntervalMillis() { return reportingIntervalMillis; } - public RateLimitResult rateLimit() { - RateLimitResult rateLimitResult = resolveStrategy().rateLimit(); - if (rateLimitResult.isAllowed()) { + public RlqsRateLimitResult rateLimit() { + boolean rateLimited = resolveStrategy().rateLimit(); + if (!rateLimited) { numRequestsAllowed.incrementAndGet(); - } else { - numRequestsDenied.incrementAndGet(); + return RlqsRateLimitResult.allow(); } - // TODO(sergiitk): [impl] when RateLimitResult broken into RlqsRateLimitResult, - // augment with deny response strategy - return rateLimitResult; + numRequestsDenied.incrementAndGet(); + return RlqsRateLimitResult.deny(denyResponse); } private RateLimitStrategy resolveStrategy() { diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index ebc23085770..763675a9af0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -23,9 +23,11 @@ import com.google.protobuf.util.Durations; import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.matchers.HttpMatchInput; +import io.grpc.xds.internal.rlqs.RlqsRateLimitResult.DenyResponse; @AutoValue public abstract class RlqsBucketSettings { + // TODO(sergiitk): [IMPL] this misses most of the parsing and implementation. public abstract ImmutableMap> bucketIdBuilder(); @@ -37,6 +39,10 @@ public RateLimitStrategy noAssignmentStrategy() { return null; } + public DenyResponse denyResponse() { + return DenyResponse.DEFAULT; + } + public RateLimitStrategy expiredAssignmentStrategy() { return null; } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index d341f7b36ad..34c25f76e5a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -50,12 +50,12 @@ public final class RlqsClient { this.bucketCache = bucketCache; } - RateLimitResult sendInitialReport(RlqsBucket bucket) { + RlqsRateLimitResult sendInitialReport(RlqsBucket bucket) { bucketCache.insertBucket(bucket); // Register the first request to the bucket for the initial report. - RateLimitResult rateLimitResult = bucket.rateLimit(); + RlqsRateLimitResult rlqsRateLimitResult = bucket.rateLimit(); rlqsStream.reportUsage(ImmutableList.of(bucket.snapshotAndResetUsage())); - return rateLimitResult; + return rlqsRateLimitResult; } void sendUsageReports(List buckets) { diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index da85dd8a5b9..a03fe4b5a4f 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -48,7 +48,7 @@ public RlqsEngine( rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); } - public RateLimitResult rateLimit(HttpMatchInput input) { + public RlqsRateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); RlqsBucket bucket = bucketCache.getBucket(bucketId); @@ -56,9 +56,9 @@ public RateLimitResult rateLimit(HttpMatchInput input) { return bucket.rateLimit(); } bucket = new RlqsBucket(bucketId, bucketSettings); - RateLimitResult rateLimitResult = rlqsClient.sendInitialReport(bucket); + RlqsRateLimitResult rlqsRateLimitResult = rlqsClient.sendInitialReport(bucket); registerReportTimer(bucketSettings.reportingIntervalMillis()); - return rateLimitResult; + return rlqsRateLimitResult; } private void registerReportTimer(final long reportingIntervalMillis) { diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsRateLimitResult.java similarity index 73% rename from xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java rename to xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsRateLimitResult.java index ccecd2b0707..803be9c9f8d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RateLimitResult.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsRateLimitResult.java @@ -23,7 +23,7 @@ import javax.annotation.Nullable; @AutoValue -public abstract class RateLimitResult { +public abstract class RlqsRateLimitResult { // TODO(sergiitk): make RateLimitResult an interface, // RlqsRateLimitResult extends it - which contains DenyResponse. @@ -37,33 +37,32 @@ public final boolean isDenied() { return denyResponse().isPresent(); } - public static RateLimitResult deny(@Nullable DenyResponse denyResponse) { + public static RlqsRateLimitResult deny(@Nullable DenyResponse denyResponse) { if (denyResponse == null) { - denyResponse = DenyResponse.create(); + denyResponse = DenyResponse.DEFAULT; } - return new AutoValue_RateLimitResult(Optional.of(denyResponse)); + return new AutoValue_RlqsRateLimitResult(Optional.of(denyResponse)); } - public static RateLimitResult allow() { - return new AutoValue_RateLimitResult(Optional.empty()); + public static RlqsRateLimitResult allow() { + return new AutoValue_RlqsRateLimitResult(Optional.empty()); } @AutoValue public abstract static class DenyResponse { + public static final DenyResponse DEFAULT = + DenyResponse.create(Status.UNAVAILABLE.withDescription("")); + public abstract Status status(); public abstract Metadata headersToAdd(); public static DenyResponse create(Status status, Metadata headersToAdd) { - return new AutoValue_RateLimitResult_DenyResponse(status, headersToAdd); + return new AutoValue_RlqsRateLimitResult_DenyResponse(status, headersToAdd); } public static DenyResponse create(Status status) { return create(status, new Metadata()); } - - public static DenyResponse create() { - return create(Status.UNAVAILABLE.withDescription("")); - } } } From 748dfcfb99857880bed677b50f58a1434091b360 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 13:07:37 -0700 Subject: [PATCH 28/47] More API improvements --- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 40 +++++++------------ .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 24 +++++++---- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 34c25f76e5a..5a77298308a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -16,8 +16,8 @@ package io.grpc.xds.internal.rlqs; -import com.google.common.collect.ImmutableList; import com.google.protobuf.util.Durations; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.BucketId; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction; @@ -31,6 +31,7 @@ import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import io.grpc.xds.internal.datatype.RateLimitStrategy; +import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -50,29 +51,19 @@ public final class RlqsClient { this.bucketCache = bucketCache; } - RlqsRateLimitResult sendInitialReport(RlqsBucket bucket) { - bucketCache.insertBucket(bucket); - // Register the first request to the bucket for the initial report. - RlqsRateLimitResult rlqsRateLimitResult = bucket.rateLimit(); - rlqsStream.reportUsage(ImmutableList.of(bucket.snapshotAndResetUsage())); - return rlqsRateLimitResult; + void sendUsageReports(List bucketUsage) { + rlqsStream.reportUsage(bucketUsage); } - void sendUsageReports(List buckets) { - ImmutableList.Builder reports = ImmutableList.builder(); - for (RlqsBucket bucket : buckets) { - reports.add(bucket.snapshotAndResetUsage()); - } - rlqsStream.reportUsage(reports.build()); - } - - void abandonBucket(RlqsBucketId bucketId) { - bucketCache.deleteBucket(bucketId); + void abandonBucket(BucketId bucketId) { + bucketCache.deleteBucket(RlqsBucketId.fromEnvoyProto(bucketId)); } - void updateBucketAssignment( - RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { - bucketCache.updateBucket(bucketId, rateLimitStrategy, ttlMillis); + void updateBucketAssignment(BucketId bucketId, QuotaAssignmentAction quotaAssignment) { + bucketCache.updateBucket( + RlqsBucketId.fromEnvoyProto(bucketId), + RateLimitStrategy.fromEnvoyProto(quotaAssignment.getRateLimitStrategy()), + Durations.toMillis(quotaAssignment.getAssignmentTimeToLive())); } public void shutdown() { @@ -138,13 +129,12 @@ public void onNext(RateLimitQuotaResponse response) { for (BucketAction bucketAction : response.getBucketActionList()) { switch (bucketAction.getBucketActionCase()) { case ABANDON_ACTION: - abandonBucket(RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId())); + abandonBucket(bucketAction.getBucketId()); break; case QUOTA_ASSIGNMENT_ACTION: - QuotaAssignmentAction quotaAssignmentAction = bucketAction.getQuotaAssignmentAction(); - updateBucketAssignment(RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId()), - RateLimitStrategy.fromEnvoyProto(quotaAssignmentAction.getRateLimitStrategy()), - Durations.toMillis(quotaAssignmentAction.getAssignmentTimeToLive())); + updateBucketAssignment( + bucketAction.getBucketId(), + bucketAction.getQuotaAssignmentAction()); break; default: // TODO(sergiitk): error diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index a03fe4b5a4f..cb5acd2768d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -20,6 +20,7 @@ import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; +import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -34,20 +35,21 @@ public class RlqsEngine { private final Matcher bucketMatchers; private final RlqsBucketCache bucketCache; private final String configHash; - private final ScheduledExecutorService timeService; + private final ScheduledExecutorService scheduler; private final ConcurrentHashMap> timers = new ConcurrentHashMap<>(); public RlqsEngine( RemoteServerInfo rlqsServer, String domain, Matcher bucketMatchers, String configHash, - ScheduledExecutorService timeService) { + ScheduledExecutorService scheduler) { this.bucketMatchers = bucketMatchers; this.configHash = configHash; - this.timeService = timeService; + this.scheduler = scheduler; bucketCache = new RlqsBucketCache(); rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); } + // TODO(sergiitk): Instead, we should do something similar to computeIfAbsent(). public RlqsRateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); @@ -55,8 +57,12 @@ public RlqsRateLimitResult rateLimit(HttpMatchInput input) { if (bucket != null) { return bucket.rateLimit(); } + // Create the new bucket. bucket = new RlqsBucket(bucketId, bucketSettings); - RlqsRateLimitResult rlqsRateLimitResult = rlqsClient.sendInitialReport(bucket); + bucketCache.insertBucket(bucket); + // Register the first request to the bucket before the initial report. + RlqsRateLimitResult rlqsRateLimitResult = bucket.rateLimit(); + rlqsClient.sendUsageReports(ImmutableList.of(bucket.snapshotAndResetUsage())); registerReportTimer(bucketSettings.reportingIntervalMillis()); return rlqsRateLimitResult; } @@ -67,7 +73,7 @@ private void registerReportTimer(final long reportingIntervalMillis) { return; } // TODO(sergiitk): [IMPL] consider manually extending. - ScheduledFuture schedule = timeService.scheduleWithFixedDelay( + ScheduledFuture schedule = scheduler.scheduleWithFixedDelay( () -> reportBucketsWithInterval(reportingIntervalMillis), reportingIntervalMillis, reportingIntervalMillis, @@ -76,10 +82,12 @@ private void registerReportTimer(final long reportingIntervalMillis) { } private void reportBucketsWithInterval(long reportingIntervalMillis) { - ImmutableList bucketsToReport = - bucketCache.getBucketsToReport(reportingIntervalMillis); + ImmutableList.Builder reports = ImmutableList.builder(); + for (RlqsBucket bucket : bucketCache.getBucketsToReport(reportingIntervalMillis)) { + reports.add(bucket.snapshotAndResetUsage()); + } // TODO(sergiitk): [IMPL] destroy timer if empty - rlqsClient.sendUsageReports(bucketsToReport); + rlqsClient.sendUsageReports(reports.build()); } public void shutdown() { From 03bde29d1bb97b2a21cde6b37ed25157b39c5546 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 15:44:40 -0700 Subject: [PATCH 29/47] getOrCreate pattern for bucket cache and timers --- .../xds/internal/rlqs/RlqsBucketCache.java | 43 +++++++------- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 4 ++ .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 56 ++++++++++--------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java index 6983114e7dc..6663277067d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java @@ -20,39 +20,47 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import io.grpc.xds.internal.datatype.RateLimitStrategy; +import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; final class RlqsBucketCache { // TODO(sergiitk): consider volatile + synchronize instead private final ConcurrentMap> bucketsPerInterval = new ConcurrentHashMap<>(); private final ConcurrentMap buckets = new ConcurrentHashMap<>(); - RlqsBucket getBucket(RlqsBucketId bucketId) { - return buckets.get(bucketId); - } - - void insertBucket(RlqsBucket bucket) { + RlqsBucket getOrCreate( + RlqsBucketId bucketId, RlqsBucketSettings bucketSettings, Consumer onCreate) { // read synchronize trick - if (buckets.get(bucket.getBucketId()) != null) { - return; + RlqsBucket bucket = buckets.get(bucketId); + if (bucket != null) { + return bucket; } synchronized (this) { + bucket = new RlqsBucket(bucketId, bucketSettings); long interval = bucket.getReportingIntervalMillis(); - if (!bucketsPerInterval.containsKey(interval)) { - bucketsPerInterval.put(interval, Sets.newConcurrentHashSet()); - } - - bucketsPerInterval.get(bucket.getReportingIntervalMillis()).add(bucket); + bucketsPerInterval.computeIfAbsent(interval, k -> Sets.newConcurrentHashSet()).add(bucket); buckets.put(bucket.getBucketId(), bucket); + // TODO(sergiitk): [IMPL] call async + onCreate.accept(bucket); + return bucket; } } void deleteBucket(RlqsBucketId bucketId) { RlqsBucket bucket = buckets.get(bucketId); - bucketsPerInterval.get(bucket.getReportingIntervalMillis()).remove(bucket); - buckets.remove(bucket.getBucketId()); + if (bucket == null) { + return; + } + synchronized (this) { + buckets.remove(bucket.getBucketId()); + bucketsPerInterval.computeIfPresent(bucket.getReportingIntervalMillis(), (k, buckets) -> { + buckets.remove(bucket); + return buckets.isEmpty() ? null : buckets; + }); + } } void updateBucket(RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { @@ -61,10 +69,7 @@ void updateBucket(RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, lo } public ImmutableList getBucketsToReport(long reportingIntervalMillis) { - ImmutableList.Builder report = ImmutableList.builder(); - for (RlqsBucket bucket : bucketsPerInterval.get(reportingIntervalMillis)) { - report.add(bucket); - } - return report.build(); + return ImmutableList.copyOf( + bucketsPerInterval.getOrDefault(reportingIntervalMillis, Collections.emptySet())); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 5a77298308a..e29f24dc3be 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -52,6 +52,10 @@ public final class RlqsClient { } void sendUsageReports(List bucketUsage) { + if (bucketUsage.isEmpty()) { + return; + } + // TODO(sergiitk): [impl] offload to serialized executor. rlqsStream.reportUsage(bucketUsage); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index cb5acd2768d..fe04508fc7b 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -22,6 +22,8 @@ import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -36,7 +38,7 @@ public class RlqsEngine { private final RlqsBucketCache bucketCache; private final String configHash; private final ScheduledExecutorService scheduler; - private final ConcurrentHashMap> timers = new ConcurrentHashMap<>(); + private final ConcurrentMap> timers = new ConcurrentHashMap<>(); public RlqsEngine( RemoteServerInfo rlqsServer, String domain, @@ -49,44 +51,46 @@ public RlqsEngine( rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); } - // TODO(sergiitk): Instead, we should do something similar to computeIfAbsent(). public RlqsRateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); - RlqsBucket bucket = bucketCache.getBucket(bucketId); - if (bucket != null) { - return bucket.rateLimit(); + RlqsBucket bucket = bucketCache.getOrCreate(bucketId, bucketSettings, newBucket -> { + // Called if a new bucket was created. + scheduleImmediateReport(newBucket); + registerReportTimer(newBucket.getReportingIntervalMillis()); + }); + return bucket.rateLimit(); + } + + private void scheduleImmediateReport(RlqsBucket newBucket) { + try { + ScheduledFuture unused = scheduler.schedule( + () -> rlqsClient.sendUsageReports(ImmutableList.of(newBucket.snapshotAndResetUsage())), + 1, TimeUnit.MICROSECONDS); + } catch (RejectedExecutionException e) { + // Shouldn't happen. + logger.finer("Couldn't schedule immediate report for bucket " + newBucket.getBucketId()); } - // Create the new bucket. - bucket = new RlqsBucket(bucketId, bucketSettings); - bucketCache.insertBucket(bucket); - // Register the first request to the bucket before the initial report. - RlqsRateLimitResult rlqsRateLimitResult = bucket.rateLimit(); - rlqsClient.sendUsageReports(ImmutableList.of(bucket.snapshotAndResetUsage())); - registerReportTimer(bucketSettings.reportingIntervalMillis()); - return rlqsRateLimitResult; } - private void registerReportTimer(final long reportingIntervalMillis) { + private void registerReportTimer(final long intervalMillis) { // TODO(sergiitk): [IMPL] cap the interval. - if (timers.containsKey(reportingIntervalMillis)) { - return; - } - // TODO(sergiitk): [IMPL] consider manually extending. - ScheduledFuture schedule = scheduler.scheduleWithFixedDelay( - () -> reportBucketsWithInterval(reportingIntervalMillis), - reportingIntervalMillis, - reportingIntervalMillis, + timers.computeIfAbsent(intervalMillis, k -> newTimer(intervalMillis)); + } + + private ScheduledFuture newTimer(final long intervalMillis) { + return scheduler.scheduleWithFixedDelay( + () -> reportBucketsWithInterval(intervalMillis), + intervalMillis, + intervalMillis, TimeUnit.MILLISECONDS); - timers.put(reportingIntervalMillis, schedule); } - private void reportBucketsWithInterval(long reportingIntervalMillis) { + private void reportBucketsWithInterval(long intervalMillis) { ImmutableList.Builder reports = ImmutableList.builder(); - for (RlqsBucket bucket : bucketCache.getBucketsToReport(reportingIntervalMillis)) { + for (RlqsBucket bucket : bucketCache.getBucketsToReport(intervalMillis)) { reports.add(bucket.snapshotAndResetUsage()); } - // TODO(sergiitk): [IMPL] destroy timer if empty rlqsClient.sendUsageReports(reports.build()); } From 00ea5459de59be746183c49b2171beb6df9043ab Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 17:00:20 -0700 Subject: [PATCH 30/47] RlqsClient doesn't know about the bucket cache anymore; uses callbacks --- .../internal/datatype/RateLimitStrategy.java | 14 +++- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 44 ++++--------- .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 17 ++++- .../internal/rlqs/RlqsUpdateBucketAction.java | 65 +++++++++++++++++++ 4 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsUpdateBucketAction.java diff --git a/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java index 8cfca8cb967..75774954ec2 100644 --- a/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java +++ b/xds/src/main/java/io/grpc/xds/internal/datatype/RateLimitStrategy.java @@ -26,6 +26,11 @@ public abstract class RateLimitStrategy { // and AllowAll DenyAll, TokenBucket extending it public enum Kind { BLANKET_RULE, TOKEN_BUCKET } + public static final RateLimitStrategy ALLOW_ALL = + AutoOneOf_RateLimitStrategy.blanketRule(BlanketRule.ALLOW_ALL); + public static final RateLimitStrategy DENY_ALL = + AutoOneOf_RateLimitStrategy.blanketRule(BlanketRule.DENY_ALL); + public abstract Kind getKind(); public final boolean rateLimit() { @@ -52,7 +57,14 @@ public final boolean rateLimit() { public abstract TokenBucket tokenBucket(); public static RateLimitStrategy ofBlanketRule(BlanketRule blanketRuleProto) { - return AutoOneOf_RateLimitStrategy.blanketRule(blanketRuleProto); + switch (blanketRuleProto) { + case ALLOW_ALL: + return RateLimitStrategy.ALLOW_ALL; + case DENY_ALL: + return RateLimitStrategy.DENY_ALL; + default: + throw new UnsupportedOperationException("Wrong BlanketRule proto"); + } } public static RateLimitStrategy ofTokenBucket(TokenBucket tokenBucketProto) { diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index e29f24dc3be..b21ecbf46e5 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -16,11 +16,10 @@ package io.grpc.xds.internal.rlqs; +import com.google.common.collect.ImmutableList; import com.google.protobuf.util.Durations; -import io.envoyproxy.envoy.service.rate_limit_quota.v3.BucketId; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction; -import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaServiceGrpc; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaServiceGrpc.RateLimitQuotaServiceStub; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports; @@ -30,11 +29,11 @@ import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; -import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,16 +41,19 @@ public final class RlqsClient { private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); private final RemoteServerInfo serverInfo; + private final Consumer> bucketsUpdateCallback; private final RlqsStream rlqsStream; - private final RlqsBucketCache bucketCache; - RlqsClient(RemoteServerInfo serverInfo, String domain, RlqsBucketCache bucketCache) { + RlqsClient( + RemoteServerInfo serverInfo, String domain, + Consumer> bucketsUpdateCallback) { + // TODO(sergiitk): [post] check not null. this.serverInfo = serverInfo; + this.bucketsUpdateCallback = bucketsUpdateCallback; this.rlqsStream = new RlqsStream(serverInfo, domain); - this.bucketCache = bucketCache; } - void sendUsageReports(List bucketUsage) { + public void sendUsageReports(List bucketUsage) { if (bucketUsage.isEmpty()) { return; } @@ -59,17 +61,6 @@ void sendUsageReports(List bucketUsage) { rlqsStream.reportUsage(bucketUsage); } - void abandonBucket(BucketId bucketId) { - bucketCache.deleteBucket(RlqsBucketId.fromEnvoyProto(bucketId)); - } - - void updateBucketAssignment(BucketId bucketId, QuotaAssignmentAction quotaAssignment) { - bucketCache.updateBucket( - RlqsBucketId.fromEnvoyProto(bucketId), - RateLimitStrategy.fromEnvoyProto(quotaAssignment.getRateLimitStrategy()), - Durations.toMillis(quotaAssignment.getAssignmentTimeToLive())); - } - public void shutdown() { logger.log(Level.FINER, "Shutting down RlqsClient to {0}", serverInfo.target()); // TODO(sergiitk): [IMPL] RlqsClient shutdown @@ -97,8 +88,6 @@ private class RlqsStream { clientCallStream = (ClientCallStreamObserver) stub.streamRateLimitQuotas(new RlqsStreamObserver()); // TODO(sergiitk): [IMPL] set on ready handler? - // TODO(sergiitk): [QUESTION] a nice way to handle setting domain in the first usage report? - // - probably an interceptor } private BucketQuotaUsage toUsageReport(RlqsBucket.RlqsBucketUsage usage) { @@ -130,20 +119,11 @@ void reportUsage(List usageReports) { private class RlqsStreamObserver implements StreamObserver { @Override public void onNext(RateLimitQuotaResponse response) { + ImmutableList.Builder bucketUpdates = ImmutableList.builder(); for (BucketAction bucketAction : response.getBucketActionList()) { - switch (bucketAction.getBucketActionCase()) { - case ABANDON_ACTION: - abandonBucket(bucketAction.getBucketId()); - break; - case QUOTA_ASSIGNMENT_ACTION: - updateBucketAssignment( - bucketAction.getBucketId(), - bucketAction.getQuotaAssignmentAction()); - break; - default: - // TODO(sergiitk): error - } + bucketUpdates.add(RlqsUpdateBucketAction.fromEnvoyProto(bucketAction)); } + bucketsUpdateCallback.accept(bucketUpdates.build()); } @Override diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index fe04508fc7b..c1090110afd 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -18,9 +18,11 @@ import com.google.common.collect.ImmutableList; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.RejectedExecutionException; @@ -48,7 +50,7 @@ public RlqsEngine( this.configHash = configHash; this.scheduler = scheduler; bucketCache = new RlqsBucketCache(); - rlqsClient = new RlqsClient(rlqsServer, domain, bucketCache); + rlqsClient = new RlqsClient(rlqsServer, domain, this::onBucketsUpdate); } public RlqsRateLimitResult rateLimit(HttpMatchInput input) { @@ -62,6 +64,19 @@ public RlqsRateLimitResult rateLimit(HttpMatchInput input) { return bucket.rateLimit(); } + private void onBucketsUpdate(List bucketActions) { + // TODO(sergiitk): [impl] ensure no more than 1 update at a time. + for (RlqsUpdateBucketAction bucketAction : bucketActions) { + RlqsBucketId bucketId = bucketAction.bucketId(); + RateLimitStrategy rateLimitStrategy = bucketAction.rateLimitStrategy(); + if (rateLimitStrategy == null) { + bucketCache.deleteBucket(bucketId); + continue; + } + bucketCache.updateBucket(bucketId, rateLimitStrategy, bucketAction.ttlMillis()); + } + } + private void scheduleImmediateReport(RlqsBucket newBucket) { try { ScheduledFuture unused = scheduler.schedule( diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsUpdateBucketAction.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsUpdateBucketAction.java new file mode 100644 index 00000000000..e39a3b90f80 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsUpdateBucketAction.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.rlqs; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.protobuf.util.Durations; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse; +import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction; +import io.grpc.xds.internal.datatype.RateLimitStrategy; +import javax.annotation.Nullable; + +@AutoValue +public abstract class RlqsUpdateBucketAction { + + public abstract RlqsBucketId bucketId(); + + @Nullable public abstract RateLimitStrategy rateLimitStrategy(); + + public abstract long ttlMillis(); + + public static RlqsUpdateBucketAction ofQuotaAssignmentAction( + RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { + Preconditions.checkNotNull(rateLimitStrategy, "rateLimitStrategy"); + return new AutoValue_RlqsUpdateBucketAction(bucketId, rateLimitStrategy, ttlMillis); + } + + public static RlqsUpdateBucketAction ofQuotaAbandonAction(RlqsBucketId bucketId) { + return new AutoValue_RlqsUpdateBucketAction(bucketId, null, 0); + } + + public static RlqsUpdateBucketAction fromEnvoyProto( + RateLimitQuotaResponse.BucketAction bucketAction) { + RlqsBucketId bucketId = RlqsBucketId.fromEnvoyProto(bucketAction.getBucketId()); + switch (bucketAction.getBucketActionCase()) { + case ABANDON_ACTION: + return RlqsUpdateBucketAction.ofQuotaAbandonAction(bucketId); + case QUOTA_ASSIGNMENT_ACTION: + QuotaAssignmentAction quotaAssignment = bucketAction.getQuotaAssignmentAction(); + RateLimitStrategy strategy = RateLimitStrategy.ALLOW_ALL; + if (quotaAssignment.hasRateLimitStrategy()) { + strategy = RateLimitStrategy.fromEnvoyProto(quotaAssignment.getRateLimitStrategy()); + } + return RlqsUpdateBucketAction.ofQuotaAssignmentAction(bucketId, strategy, + Durations.toMillis(quotaAssignment.getAssignmentTimeToLive())); + default: + // TODO(sergiitk): [impl] error + throw new UnsupportedOperationException("Wrong BlanketRule proto"); + } + } +} From c567c46270b6e5072211bf52af586ecc8596ce87 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 18:36:09 -0700 Subject: [PATCH 31/47] Minor renames --- .../java/io/grpc/xds/internal/rlqs/RlqsClient.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index b21ecbf46e5..1ab7f710ca8 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -53,12 +53,12 @@ public final class RlqsClient { this.rlqsStream = new RlqsStream(serverInfo, domain); } - public void sendUsageReports(List bucketUsage) { - if (bucketUsage.isEmpty()) { + public void sendUsageReports(List bucketUsages) { + if (bucketUsages.isEmpty()) { return; } // TODO(sergiitk): [impl] offload to serialized executor. - rlqsStream.reportUsage(bucketUsage); + rlqsStream.reportUsage(bucketUsages); } public void shutdown() { @@ -119,11 +119,11 @@ void reportUsage(List usageReports) { private class RlqsStreamObserver implements StreamObserver { @Override public void onNext(RateLimitQuotaResponse response) { - ImmutableList.Builder bucketUpdates = ImmutableList.builder(); + ImmutableList.Builder updateActions = ImmutableList.builder(); for (BucketAction bucketAction : response.getBucketActionList()) { - bucketUpdates.add(RlqsUpdateBucketAction.fromEnvoyProto(bucketAction)); + updateActions.add(RlqsUpdateBucketAction.fromEnvoyProto(bucketAction)); } - bucketsUpdateCallback.accept(bucketUpdates.build()); + bucketsUpdateCallback.accept(updateActions.build()); } @Override From 831cd88bba98afd01134aae54ec59d2dab22d556 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 19:53:51 -0700 Subject: [PATCH 32/47] improved getOrCreateRlqsEngine --- .../io/grpc/xds/internal/rlqs/RlqsBucket.java | 2 +- .../xds/internal/rlqs/RlqsBucketCache.java | 7 +-- .../io/grpc/xds/internal/rlqs/RlqsCache.java | 45 ++++++------------- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 10f6469b509..25a93892932 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -30,12 +30,12 @@ public class RlqsBucket { private final RateLimitStrategy noAssignmentStrategy; private final RateLimitStrategy expiredAssignmentStrategy; + private final DenyResponse denyResponse; // TODO(sergiitk): [impl] consider AtomicLongFieldUpdater private final AtomicLong lastSnapshotTimeNanos = new AtomicLong(-1); private final AtomicLong numRequestsAllowed = new AtomicLong(); private final AtomicLong numRequestsDenied = new AtomicLong(); - private final DenyResponse denyResponse; // TODO(sergiitk): [impl] consider AtomicReferenceFieldUpdater @Nullable diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java index 6663277067d..6faf41ca984 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketCache.java @@ -31,7 +31,7 @@ final class RlqsBucketCache { private final ConcurrentMap> bucketsPerInterval = new ConcurrentHashMap<>(); private final ConcurrentMap buckets = new ConcurrentHashMap<>(); - RlqsBucket getOrCreate( + public RlqsBucket getOrCreate( RlqsBucketId bucketId, RlqsBucketSettings bucketSettings, Consumer onCreate) { // read synchronize trick RlqsBucket bucket = buckets.get(bucketId); @@ -49,7 +49,7 @@ RlqsBucket getOrCreate( } } - void deleteBucket(RlqsBucketId bucketId) { + public void deleteBucket(RlqsBucketId bucketId) { RlqsBucket bucket = buckets.get(bucketId); if (bucket == null) { return; @@ -63,7 +63,8 @@ void deleteBucket(RlqsBucketId bucketId) { } } - void updateBucket(RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { + public void updateBucket( + RlqsBucketId bucketId, RateLimitStrategy rateLimitStrategy, long ttlMillis) { RlqsBucket bucket = buckets.get(bucketId); bucket.updateAction(rateLimitStrategy, ttlMillis); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 359a4689fc1..270ba189194 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -19,17 +19,14 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.Sets; -import com.google.common.util.concurrent.SettableFuture; +import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.SynchronizationContext; import io.grpc.xds.RlqsFilterConfig; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; @@ -80,34 +77,20 @@ public void shutdownRlqsEngine(RlqsFilterConfig oldConfig) { // TODO(sergiitk): shutdown one } - public RlqsEngine getOrCreateRlqsEngine(RlqsFilterConfig config) { - final String configHash = hashRlqsFilterConfig(config); - if (enginePool.containsKey(configHash)) { - return enginePool.get(configHash); - } + public RlqsEngine getOrCreateRlqsEngine(final RlqsFilterConfig config) { + String configHash = hashRlqsFilterConfig(config); + return enginePool.computeIfAbsent(configHash, k -> newRlqsEngine(k, config)); + } - final SettableFuture future = SettableFuture.create(); - syncContext.execute(() -> { - // TODO(sergiitk): [IMPL] get channel creds from the bootstrap. - RemoteServerInfo rlqsServer = RemoteServerInfo.create(config.rlqsService().targetUri(), - InsecureChannelCredentials.create()); - RlqsEngine rlqsEngine = new RlqsEngine( - rlqsServer, - config.domain(), - config.bucketMatchers(), - configHash, - scheduler); - - enginePool.put(configHash, rlqsEngine); - future.set(enginePool.get(configHash)); - }); - try { - // TODO(sergiitk): [IMPL] clarify time - return future.get(1, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - // TODO(sergiitk): [IMPL] handle properly - throw new RuntimeException(e); - } + private RlqsEngine newRlqsEngine(String configHash, RlqsFilterConfig config) { + // TODO(sergiitk): [IMPL] get channel creds from the bootstrap. + ChannelCredentials creds = InsecureChannelCredentials.create(); + return new RlqsEngine( + RemoteServerInfo.create(config.rlqsService().targetUri(), creds), + config.domain(), + config.bucketMatchers(), + configHash, + scheduler); } private String hashRlqsFilterConfig(RlqsFilterConfig config) { From c318d2fc88da1c5d36c4286d539c33699e5aa2f1 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 27 Sep 2024 20:46:06 -0700 Subject: [PATCH 33/47] Handle special case --- .../java/io/grpc/xds/internal/rlqs/RlqsEngine.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index c1090110afd..8b1f159ec07 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -56,6 +56,10 @@ public RlqsEngine( public RlqsRateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); + // Special case when bucket id builder not set. + if (bucketId == null) { + return rateLimitWithoutReports(bucketSettings); + } RlqsBucket bucket = bucketCache.getOrCreate(bucketId, bucketSettings, newBucket -> { // Called if a new bucket was created. scheduleImmediateReport(newBucket); @@ -64,6 +68,13 @@ public RlqsRateLimitResult rateLimit(HttpMatchInput input) { return bucket.rateLimit(); } + private static RlqsRateLimitResult rateLimitWithoutReports(RlqsBucketSettings bucketSettings) { + if (bucketSettings.noAssignmentStrategy().rateLimit()) { + return RlqsRateLimitResult.deny(bucketSettings.denyResponse()); + } + return RlqsRateLimitResult.allow(); + } + private void onBucketsUpdate(List bucketActions) { // TODO(sergiitk): [impl] ensure no more than 1 update at a time. for (RlqsUpdateBucketAction bucketAction : bucketActions) { From 9b0b8ebb600a29b65e9dc04c003cd92d7cf2d917 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 3 Oct 2024 11:41:07 -0700 Subject: [PATCH 34/47] Dynamic bucket id builder processing initial logic. --- .../grpc/xds/internal/rlqs/RlqsBucketId.java | 22 ++++++---- .../xds/internal/rlqs/RlqsBucketSettings.java | 44 ++++++++++++++++--- .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 4 +- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java index 65360ce0e23..e78b36ebbd3 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketId.java @@ -24,19 +24,25 @@ @AutoValue public abstract class RlqsBucketId { + // No class loading deadlock, see + // https://github.com/google/error-prone/issues/2062#issuecomment-1566253739 + public static final RlqsBucketId EMPTY = create(ImmutableMap.of()); + public abstract ImmutableMap bucketId(); - public static RlqsBucketId create(ImmutableMap bucketId) { - return new AutoValue_RlqsBucketId(bucketId); + public static RlqsBucketId create(Map bucketIdMap) { + if (bucketIdMap.isEmpty()) { + return EMPTY; + } + return new AutoValue_RlqsBucketId(ImmutableMap.copyOf(bucketIdMap)); } - public static RlqsBucketId fromEnvoyProto(BucketId envoyProto) { - ImmutableMap.Builder bucketId = ImmutableMap.builder(); - for (Map.Entry entry : envoyProto.getBucketMap().entrySet()) { - bucketId.put(entry.getKey(), entry.getValue()); - } - return RlqsBucketId.create(bucketId.build()); + public final boolean isEmpty() { + return bucketId().isEmpty(); + } + public static RlqsBucketId fromEnvoyProto(BucketId envoyProto) { + return RlqsBucketId.create(ImmutableMap.copyOf(envoyProto.getBucketMap().entrySet())); } @Memoized diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java index 763675a9af0..da202de011c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucketSettings.java @@ -17,22 +17,31 @@ package io.grpc.xds.internal.rlqs; import com.google.auto.value.AutoValue; -import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.rlqs.RlqsRateLimitResult.DenyResponse; +import java.util.function.Function; +import javax.annotation.Nullable; @AutoValue public abstract class RlqsBucketSettings { // TODO(sergiitk): [IMPL] this misses most of the parsing and implementation. + @Nullable public abstract ImmutableMap> bucketIdBuilder(); - public RlqsBucketId toBucketId(HttpMatchInput input) { - return null; + abstract RlqsBucketId staticBucketId(); + + public abstract long reportingIntervalMillis(); + + public final RlqsBucketId toBucketId(HttpMatchInput input) { + if (bucketIdBuilder() == null) { + return staticBucketId(); + } + return processBucketBuilder(bucketIdBuilder(), input); } public RateLimitStrategy noAssignmentStrategy() { @@ -47,11 +56,34 @@ public RateLimitStrategy expiredAssignmentStrategy() { return null; } - public abstract long reportingIntervalMillis(); - public static RlqsBucketSettings create( ImmutableMap> bucketIdBuilder, Duration reportingInterval) { - return new AutoValue_RlqsBucketSettings(bucketIdBuilder, Durations.toMillis(reportingInterval)); + // TODO(sergiitk): instead of create, use Builder pattern. + RlqsBucketId staticBucketId = processBucketBuilder(bucketIdBuilder, null); + return new AutoValue_RlqsBucketSettings( + staticBucketId.isEmpty() ? bucketIdBuilder : null, + staticBucketId, + Durations.toMillis(reportingInterval)); + } + + private static RlqsBucketId processBucketBuilder( + ImmutableMap> bucketIdBuilder, + HttpMatchInput input) { + ImmutableMap.Builder bucketIdMapBuilder = ImmutableMap.builder(); + if (input == null) { + // TODO(sergiitk): [IMPL] calculate static map + return RlqsBucketId.EMPTY; + } + for (String key : bucketIdBuilder.keySet()) { + Function fn = bucketIdBuilder.get(key); + String value = null; + if (fn != null) { + value = fn.apply(input); + } + bucketIdMapBuilder.put(key, value != null ? value : ""); + } + ImmutableMap bucketIdMap = bucketIdMapBuilder.build(); + return bucketIdMap.isEmpty() ? RlqsBucketId.EMPTY : RlqsBucketId.create(bucketIdMap); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index 8b1f159ec07..d0b2021fa93 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -56,8 +56,8 @@ public RlqsEngine( public RlqsRateLimitResult rateLimit(HttpMatchInput input) { RlqsBucketSettings bucketSettings = bucketMatchers.match(input); RlqsBucketId bucketId = bucketSettings.toBucketId(input); - // Special case when bucket id builder not set. - if (bucketId == null) { + // Special case when bucket id builder not set, or has no values. + if (bucketId.isEmpty()) { return rateLimitWithoutReports(bucketSettings); } RlqsBucket bucket = bucketCache.getOrCreate(bucketId, bucketSettings, newBucket -> { From 9d9e279554f84799f1cbfb06c2dcb1d66d6654f1 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 4 Oct 2024 12:36:46 -0700 Subject: [PATCH 35/47] hash config to a long --- .../main/java/io/grpc/xds/RlqsFilterConfig.java | 2 ++ .../java/io/grpc/xds/internal/rlqs/RlqsCache.java | 15 +++++++++------ .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java index 1ccc911c9ac..1ffb88709f9 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilterConfig.java @@ -35,9 +35,11 @@ public final String typeUrl() { public abstract String domain(); + // TODO(sergiitk): make not nullable, introduce RlqsFilterConfigOverride @Nullable public abstract GrpcService rlqsService(); + // TODO(sergiitk): make not nullable, introduce RlqsFilterConfigOverride @Nullable public abstract Matcher bucketMatchers(); diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 270ba189194..a2b98cd9eff 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -24,6 +24,7 @@ import io.grpc.SynchronizationContext; import io.grpc.xds.RlqsFilterConfig; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; @@ -41,7 +42,7 @@ public final class RlqsCache { throw new RlqsPoolSynchronizationException(message, error); }); - private final ConcurrentHashMap enginePool = new ConcurrentHashMap<>(); + private final ConcurrentHashMap enginePool = new ConcurrentHashMap<>(); Set enginesToShutdown = Sets.newConcurrentHashSet(); private final ScheduledExecutorService scheduler; @@ -65,7 +66,7 @@ public void shutdown() { shutdown = true; logger.log(Level.FINER, "Shutting down RlqsCache"); enginesToShutdown.clear(); - for (String configHash : enginePool.keySet()) { + for (long configHash : enginePool.keySet()) { enginePool.get(configHash).shutdown(); } enginePool.clear(); @@ -78,11 +79,11 @@ public void shutdownRlqsEngine(RlqsFilterConfig oldConfig) { } public RlqsEngine getOrCreateRlqsEngine(final RlqsFilterConfig config) { - String configHash = hashRlqsFilterConfig(config); + long configHash = hashRlqsFilterConfig(config); return enginePool.computeIfAbsent(configHash, k -> newRlqsEngine(k, config)); } - private RlqsEngine newRlqsEngine(String configHash, RlqsFilterConfig config) { + private RlqsEngine newRlqsEngine(long configHash, RlqsFilterConfig config) { // TODO(sergiitk): [IMPL] get channel creds from the bootstrap. ChannelCredentials creds = InsecureChannelCredentials.create(); return new RlqsEngine( @@ -93,11 +94,13 @@ private RlqsEngine newRlqsEngine(String configHash, RlqsFilterConfig config) { scheduler); } - private String hashRlqsFilterConfig(RlqsFilterConfig config) { + private long hashRlqsFilterConfig(RlqsFilterConfig config) { // TODO(sergiitk): [QUESTION] better name? - ask Eric. // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? // TODO(sergiitk): [IMPL] Hash buckets - return config.rlqsService().targetUri() + config.domain(); + int k1 = Objects.hash(config.rlqsService().targetUri(), config.domain()); + int k2 = config.bucketMatchers().hashCode(); + return Long.rotateLeft(Integer.toUnsignedLong(k1), 32) + Integer.toUnsignedLong(k2); } /** diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index d0b2021fa93..70570fbd2b9 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -38,13 +38,13 @@ public class RlqsEngine { private final RlqsClient rlqsClient; private final Matcher bucketMatchers; private final RlqsBucketCache bucketCache; - private final String configHash; + private final long configHash; private final ScheduledExecutorService scheduler; private final ConcurrentMap> timers = new ConcurrentHashMap<>(); public RlqsEngine( RemoteServerInfo rlqsServer, String domain, - Matcher bucketMatchers, String configHash, + Matcher bucketMatchers, long configHash, ScheduledExecutorService scheduler) { this.bucketMatchers = bucketMatchers; this.configHash = configHash; From 3d9c5cb10ce9c143a17313c26e6962e1faa1412f Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 8 Oct 2024 11:06:02 -0700 Subject: [PATCH 36/47] Remove outdated note --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index a49cc31dc44..8a015e43ba7 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -152,24 +152,6 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { @Override public Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - // Notes: - // map domain() -> an incarnation of bucket matchers, f.e. new RlqsEngine(domain, matchers). - // shared resource holder, acquire every rpc - // Store RLQS Client or channel in the config as a reference - FilterConfig config ref - // when parse. - // - atomic maybe - // - allocate channel on demand / ref counting - // - and interface to notify service interceptor on shutdown - // - destroy channel when ref count 0 - // potentially many RLQS Clients sharing a channel to grpc RLQS service - - // TODO(sergiitk): [QUESTION] look up how cache is looked up - // now we create filters every RPC. will be change in RBAC. - // we need to avoid recreating filter when config doesn't change - // m: trigger close() after we create new instances - // RBAC filter recreate? - has to be fixed for RBAC - // AI: follow up with Eric on how cache is shared, this changes if we need to cache - // interceptor - // AI: discuss the lifetime of RLQS channel and the cache - needs wider per-lang discussion. RlqsRateLimitResult result = rlqsEngine.rateLimit(HttpMatchInput.create(headers, call)); if (result.isAllowed()) { return next.startCall(call, headers); From e1e88787b0936c5d857a7290ef24cd2b1d4a6a12 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 15 Oct 2024 10:17:57 -0700 Subject: [PATCH 37/47] add Filter.isEnabled() --- xds/src/main/java/io/grpc/xds/Filter.java | 4 ++++ xds/src/main/java/io/grpc/xds/FilterRegistry.java | 3 +++ xds/src/main/java/io/grpc/xds/RlqsFilter.java | 8 +++++++- xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 9383f6df63a..7383de67a5a 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -38,6 +38,10 @@ interface Filter { */ String[] typeUrls(); + default boolean isEnabled() { + return true; + } + /** * Parses the top-level filter config from raw proto message. The message may be either a {@link * com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index ae932746761..033002e506f 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -51,6 +51,9 @@ static FilterRegistry newRegistry() { @VisibleForTesting FilterRegistry register(Filter... filters) { for (Filter filter : filters) { + if (!filter.isEnabled()) { + continue; + } for (String typeUrl : filter.typeUrls()) { supportedFilters.put(typeUrl, filter); } diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 8a015e43ba7..15b0106cdf9 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -32,6 +32,7 @@ import io.grpc.ServerCall.Listener; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import io.grpc.internal.GrpcUtil; import io.grpc.xds.Filter.ServerInterceptorBuilder; import io.grpc.xds.internal.datatype.GrpcService; import io.grpc.xds.internal.matchers.HttpMatchInput; @@ -50,7 +51,7 @@ // TODO(sergiitk): introduce a layer between the filter and interceptor. // lds has filter names and the names are unique - even for server instances. final class RlqsFilter implements Filter, ServerInterceptorBuilder { - // private static final Logger logger = Logger.getLogger(RlqsFilter.class.getName()); + static final boolean enabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_ENABLE_RLQS", false); static final RlqsFilter INSTANCE = new RlqsFilter(); @@ -66,6 +67,11 @@ public String[] typeUrls() { return new String[]{TYPE_URL, TYPE_URL_OVERRIDE_CONFIG}; } + @Override + public boolean isEnabled() { + return enabled; + } + @Override public ConfigOrError parseFilterConfig(Message rawProtoMessage) { try { diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index b06195b9abf..a7b260214dc 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -1119,6 +1119,8 @@ public void run() { Filter filter = mock(Filter.class, withSettings() .extraInterfaces(ServerInterceptorBuilder.class)); when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filter.isEnabled()).thenCallRealMethod(); + assertThat(filter.isEnabled()).isTrue(); filterRegistry.register(filter); FilterConfig f0 = mock(FilterConfig.class); FilterConfig f0Override = mock(FilterConfig.class); @@ -1192,6 +1194,8 @@ public void run() { Filter filter = mock(Filter.class, withSettings() .extraInterfaces(ServerInterceptorBuilder.class)); when(filter.typeUrls()).thenReturn(new String[]{"filter-type-url"}); + when(filter.isEnabled()).thenCallRealMethod(); + assertThat(filter.isEnabled()).isTrue(); filterRegistry.register(filter); FilterConfig f0 = mock(FilterConfig.class); FilterConfig f0Override = mock(FilterConfig.class); From 0848a63f2d8e536780faca773bc08a113e378fee Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Tue, 15 Oct 2024 10:24:41 -0700 Subject: [PATCH 38/47] add GRPC_EXPERIMENTAL_RLQS_DRY_RUN --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 15b0106cdf9..040922ce928 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -27,6 +27,7 @@ import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig; import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; +import io.grpc.InternalLogId; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCall.Listener; @@ -34,6 +35,8 @@ import io.grpc.ServerInterceptor; import io.grpc.internal.GrpcUtil; import io.grpc.xds.Filter.ServerInterceptorBuilder; +import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.client.XdsLogger.XdsLogLevel; import io.grpc.xds.internal.datatype.GrpcService; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; @@ -53,6 +56,10 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { static final boolean enabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_ENABLE_RLQS", false); + // TODO(sergiitk): [IMPL] remove + // Do do not fail on parsing errors, only log requests. + static final boolean dryRun = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_RLQS_DRY_RUN", false); + static final RlqsFilter INSTANCE = new RlqsFilter(); static final String TYPE_URL = "type.googleapis.com/" @@ -62,6 +69,15 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { private final AtomicReference rlqsCache = new AtomicReference<>(); + private final InternalLogId logId; + private final XdsLogger logger; + + public RlqsFilter() { + logId = InternalLogId.allocate("rlqs-filter", null); + logger = XdsLogger.withLogId(logId); + logger.log(XdsLogLevel.INFO, "Created RLQS Filter with logId=" + logId); + } + @Override public String[] typeUrls() { return new String[]{TYPE_URL, TYPE_URL_OVERRIDE_CONFIG}; @@ -158,7 +174,15 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { @Override public Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - RlqsRateLimitResult result = rlqsEngine.rateLimit(HttpMatchInput.create(headers, call)); + HttpMatchInput httpMatchInput = HttpMatchInput.create(headers, call); + + // TODO(sergiitk): [IMPL] Remove + if (dryRun) { + logger.log(XdsLogLevel.INFO, "RLQS DRY RUN: request <<" + httpMatchInput + ">>"); + return next.startCall(call, headers); + } + + RlqsRateLimitResult result = rlqsEngine.rateLimit(httpMatchInput); if (result.isAllowed()) { return next.startCall(call, headers); } @@ -170,7 +194,7 @@ public Listener interceptCall( } @VisibleForTesting - static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) + RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) throws ResourceInvalidException, InvalidProtocolBufferException { RlqsFilterConfig.Builder builder = RlqsFilterConfig.builder(); if (rlqsFilterProto.getDomain().isEmpty()) { @@ -179,6 +203,12 @@ static RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterPro builder.domain(rlqsFilterProto.getDomain()) .rlqsService(GrpcService.fromEnvoyProto(rlqsFilterProto.getRlqsServer())); + // TODO(sergiitk): [IMPL] Remove + if (dryRun) { + logger.log(XdsLogLevel.INFO, "RLQS DRY RUN: skipping matchers"); + return builder.build(); + } + // TODO(sergiitk): [IMPL] actually parse, move to RlqsBucketSettings.fromProto() RateLimitQuotaBucketSettings fallbackBucketSettingsProto = unpackAny( rlqsFilterProto.getBucketMatchers().getOnNoMatch().getAction().getTypedConfig(), From d76d24bef38d45b5b59446d7ae579a3b2b17fad2 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 16 Oct 2024 17:11:17 -0700 Subject: [PATCH 39/47] XdsTestServer: add --xds_server_mode --- .../testing/integration/XdsTestServer.java | 109 ++++++++++++------ 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java index 1bc4ff88981..a592e3e81c0 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsTestServer.java @@ -32,6 +32,7 @@ import io.grpc.ServerCredentials; import io.grpc.ServerInterceptor; import io.grpc.ServerInterceptors; +import io.grpc.ServerServiceDefinition; import io.grpc.Status; import io.grpc.gcp.csm.observability.CsmObservability; import io.grpc.health.v1.HealthCheckResponse.ServingStatus; @@ -83,6 +84,7 @@ public final class XdsTestServer { private int port = 8080; private int maintenancePort = 8080; private boolean secureMode = false; + private boolean xdsServerMode = false; private boolean enableCsmObservability; private String serverId = "java_server"; private HealthStatusManager health; @@ -143,7 +145,10 @@ void parseArgs(String[] args) { maintenancePort = Integer.valueOf(value); } else if ("secure_mode".equals(key)) { secureMode = Boolean.parseBoolean(value); - } else if ("enable_csm_observability".equals(key)) { + } else if ("xds_server_mode".equals(key)) { + xdsServerMode = Boolean.parseBoolean(value); + } + else if ("enable_csm_observability".equals(key)) { enableCsmObservability = Boolean.valueOf(value); } else if ("server_id".equals(key)) { serverId = value; @@ -164,6 +169,9 @@ void parseArgs(String[] args) { + maintenancePort); usage = true; } + if (secureMode) { + xdsServerMode = true; + } if (usage) { XdsTestServer s = new XdsTestServer(); @@ -180,6 +188,9 @@ void parseArgs(String[] args) { + " port and maintenance_port should be different for secure mode." + "\n Default: " + s.secureMode + + "\n --xds_server_mode=BOOLEAN Start in xDS Server mode." + + "\n Default: " + + s.xdsServerMode + "\n --enable_csm_observability=BOOL Enable CSM observability reporting. Default: " + s.enableCsmObservability + "\n --server_id=STRING server ID for response." @@ -213,6 +224,11 @@ void start() throws Exception { throw new RuntimeException(e); } health = new HealthStatusManager(); + ServerServiceDefinition testServiceInterceptor = ServerInterceptors.intercept( + new TestServiceImpl(serverId, host), + new TestInfoInterceptor(host)); + ServerCredentials insecureServerCreds = InsecureServerCredentials.create(); + @SuppressWarnings("deprecation") BindableService oldReflectionService = ProtoReflectionService.newInstance(); if (secureMode) { @@ -220,7 +236,7 @@ void start() throws Exception { throw new IllegalArgumentException("Secure mode only supports IPV4_IPV6 address type"); } maintenanceServer = - Grpc.newServerBuilderForPort(maintenancePort, InsecureServerCredentials.create()) + Grpc.newServerBuilderForPort(maintenancePort, insecureServerCreds) .addService(new XdsUpdateHealthServiceImpl(health)) .addService(health.getHealthService()) .addService(oldReflectionService) @@ -228,51 +244,52 @@ void start() throws Exception { .addServices(AdminInterface.getStandardServices()) .build(); maintenanceServer.start(); - server = - XdsServerBuilder.forPort( - port, XdsServerCredentials.create(InsecureServerCredentials.create())) - .addService( - ServerInterceptors.intercept( - new TestServiceImpl(serverId, host), new TestInfoInterceptor(host))) + server = XdsServerBuilder.forPort(port, XdsServerCredentials.create(insecureServerCreds)) + .addService(testServiceInterceptor) .build(); server.start(); - } else { - ServerBuilder serverBuilder; - ServerCredentials insecureServerCreds = InsecureServerCredentials.create(); - switch (addressType) { - case IPV4_IPV6: - serverBuilder = Grpc.newServerBuilderForPort(port, insecureServerCreds); - break; - case IPV4: - SocketAddress v4Address = Util.getV4Address(port); - InetSocketAddress localV4Address = new InetSocketAddress("127.0.0.1", port); - serverBuilder = NettyServerBuilder.forAddress( - localV4Address, insecureServerCreds); - if (v4Address != null && !v4Address.equals(localV4Address) ) { - ((NettyServerBuilder) serverBuilder).addListenAddress(v4Address); - } - break; - case IPV6: - List v6Addresses = Util.getV6Addresses(port); - InetSocketAddress localV6Address = new InetSocketAddress("::1", port); - serverBuilder = NettyServerBuilder.forAddress(localV6Address, insecureServerCreds); - for (SocketAddress address : v6Addresses) { - if (!address.equals(localV6Address)) { - ((NettyServerBuilder) serverBuilder).addListenAddress(address); - } + health.setStatus("", ServingStatus.SERVING); + return; + } + + ServerBuilder serverBuilder; + switch (addressType) { + case IPV4_IPV6: + serverBuilder = Grpc.newServerBuilderForPort(port, insecureServerCreds); + break; + case IPV4: + SocketAddress v4Address = Util.getV4Address(port); + InetSocketAddress localV4Address = new InetSocketAddress("127.0.0.1", port); + serverBuilder = NettyServerBuilder.forAddress( + localV4Address, insecureServerCreds); + if (v4Address != null && !v4Address.equals(localV4Address) ) { + ((NettyServerBuilder) serverBuilder).addListenAddress(v4Address); + } + break; + case IPV6: + List v6Addresses = Util.getV6Addresses(port); + InetSocketAddress localV6Address = new InetSocketAddress("::1", port); + serverBuilder = NettyServerBuilder.forAddress(localV6Address, insecureServerCreds); + for (SocketAddress address : v6Addresses) { + if (!address.equals(localV6Address)) { + ((NettyServerBuilder) serverBuilder).addListenAddress(address); } - break; - default: - throw new AssertionError("Unknown address type: " + addressType); + } + break; + default: + throw new AssertionError("Unknown address type: " + addressType); + } + + if (xdsServerMode) { + if (addressType != Util.AddressType.IPV4_IPV6) { + throw new IllegalArgumentException("xDS Server mode only supports IPV4_IPV6 address type"); } logger.info("Starting server on port " + port + " with address type " + addressType); server = serverBuilder - .addService( - ServerInterceptors.intercept( - new TestServiceImpl(serverId, host), new TestInfoInterceptor(host))) + .addService(testServiceInterceptor) .addService(new XdsUpdateHealthServiceImpl(health)) .addService(health.getHealthService()) .addService(oldReflectionService) @@ -281,7 +298,23 @@ void start() throws Exception { .build(); server.start(); maintenanceServer = null; + health.setStatus("", ServingStatus.SERVING); + return; } + + logger.info("Starting server on port " + port + " with address type " + addressType); + + server = + serverBuilder + .addService(testServiceInterceptor) + .addService(new XdsUpdateHealthServiceImpl(health)) + .addService(health.getHealthService()) + .addService(oldReflectionService) + .addService(ProtoReflectionServiceV1.newInstance()) + .addServices(AdminInterface.getStandardServices()) + .build(); + server.start(); + maintenanceServer = null; health.setStatus("", ServingStatus.SERVING); } From e5d65570530982d888719eb318f3a8f2ba752e9c Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 16 Oct 2024 21:13:42 -0700 Subject: [PATCH 40/47] convert logid to local --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index 040922ce928..b7b519422ad 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -69,11 +69,10 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { private final AtomicReference rlqsCache = new AtomicReference<>(); - private final InternalLogId logId; private final XdsLogger logger; public RlqsFilter() { - logId = InternalLogId.allocate("rlqs-filter", null); + InternalLogId logId = InternalLogId.allocate("rlqs-filter", null); logger = XdsLogger.withLogId(logId); logger.log(XdsLogLevel.INFO, "Created RLQS Filter with logId=" + logId); } From 1d222e05eec3ecb05b097ad588a05bc870855df4 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 17 Oct 2024 10:29:33 -0700 Subject: [PATCH 41/47] PSM e2e: works! --- .../main/java/io/grpc/xds/MessagePrinter.java | 5 ++ xds/src/main/java/io/grpc/xds/RlqsFilter.java | 15 +++--- .../java/io/grpc/xds/XdsServerWrapper.java | 5 +- .../io/grpc/xds/client/XdsClientImpl.java | 6 ++- .../io/grpc/xds/internal/rlqs/RlqsCache.java | 7 ++- .../io/grpc/xds/internal/rlqs/RlqsClient.java | 50 +++++++++++++------ .../io/grpc/xds/internal/rlqs/RlqsEngine.java | 20 +++++--- 7 files changed, 77 insertions(+), 31 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/MessagePrinter.java b/xds/src/main/java/io/grpc/xds/MessagePrinter.java index 5927bfd517e..ea45cdb7c3a 100644 --- a/xds/src/main/java/io/grpc/xds/MessagePrinter.java +++ b/xds/src/main/java/io/grpc/xds/MessagePrinter.java @@ -28,6 +28,8 @@ import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.envoyproxy.envoy.extensions.clusters.aggregate.v3.ClusterConfig; import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; +import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig; +import io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaOverride; import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC; import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; @@ -58,6 +60,9 @@ private static JsonFormat.Printer newPrinter() { .add(RBAC.getDescriptor()) .add(RBACPerRoute.getDescriptor()) .add(Router.getDescriptor()) + // RLQS + .add(RateLimitQuotaFilterConfig.getDescriptor()) + .add(RateLimitQuotaOverride.getDescriptor()) // UpstreamTlsContext and DownstreamTlsContext in v3 are not transitively imported // by top-level resource types. .add(UpstreamTlsContext.getDescriptor()) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index b7b519422ad..e6d06eb9bca 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -54,6 +54,8 @@ // TODO(sergiitk): introduce a layer between the filter and interceptor. // lds has filter names and the names are unique - even for server instances. final class RlqsFilter implements Filter, ServerInterceptorBuilder { + private final XdsLogger logger; + static final boolean enabled = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_ENABLE_RLQS", false); // TODO(sergiitk): [IMPL] remove @@ -69,12 +71,11 @@ final class RlqsFilter implements Filter, ServerInterceptorBuilder { private final AtomicReference rlqsCache = new AtomicReference<>(); - private final XdsLogger logger; - public RlqsFilter() { - InternalLogId logId = InternalLogId.allocate("rlqs-filter", null); - logger = XdsLogger.withLogId(logId); - logger.log(XdsLogLevel.INFO, "Created RLQS Filter with logId=" + logId); + // TODO(sergiitk): one per new instance when filters are refactored. + logger = XdsLogger.withLogId(InternalLogId.allocate(this.getClass(), null)); + logger.log(XdsLogLevel.DEBUG, + "Created RLQS Filter with enabled=" + enabled + ", dryRun=" + dryRun); } @Override @@ -177,7 +178,7 @@ public Listener interceptCall( // TODO(sergiitk): [IMPL] Remove if (dryRun) { - logger.log(XdsLogLevel.INFO, "RLQS DRY RUN: request <<" + httpMatchInput + ">>"); + // logger.log(XdsLogLevel.INFO, "RLQS DRY RUN: request <<" + httpMatchInput + ">>"); return next.startCall(call, headers); } @@ -204,7 +205,7 @@ RlqsFilterConfig parseRlqsFilter(RateLimitQuotaFilterConfig rlqsFilterProto) // TODO(sergiitk): [IMPL] Remove if (dryRun) { - logger.log(XdsLogLevel.INFO, "RLQS DRY RUN: skipping matchers"); + logger.log(XdsLogLevel.DEBUG, "Dry run: not parsing matchers in the filter filter"); return builder.build(); } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 7f2c4e422c8..f27da13e7bb 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -59,6 +59,7 @@ import java.io.IOException; import java.net.SocketAddress; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -82,7 +83,9 @@ final class XdsServerWrapper extends Server { new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { - logger.log(Level.SEVERE, "Exception!" + e); + logger.log(Level.SEVERE, "Exception! " + + e + "\nTrace:\n" + + Arrays.toString(e.getStackTrace()).replace(',', '\n')); // TODO(chengyuanzhang): implement cleanup. } }); diff --git a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java index e2380b9ed73..6a30d9ef38a 100644 --- a/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java @@ -44,6 +44,7 @@ import io.grpc.xds.client.XdsClient.XdsResponseHandler; import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.net.URI; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -72,8 +73,9 @@ public final class XdsClientImpl extends XdsClient implements XdsResponseHandler public void uncaughtException(Thread t, Throwable e) { logger.log( XdsLogLevel.ERROR, - "Uncaught exception in XdsClient SynchronizationContext. Panic!", - e); + "Uncaught exception in XdsClient SynchronizationContext. Panic! " + + e + "\nTrace:\n" + + Arrays.toString(e.getStackTrace()).replace(',', '\n')); // TODO(chengyuanzhang): better error handling. throw new AssertionError(e); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index a2b98cd9eff..8b7fa1c8f35 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -99,7 +99,12 @@ private long hashRlqsFilterConfig(RlqsFilterConfig config) { // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? // TODO(sergiitk): [IMPL] Hash buckets int k1 = Objects.hash(config.rlqsService().targetUri(), config.domain()); - int k2 = config.bucketMatchers().hashCode(); + int k2; + if (config.bucketMatchers() == null) { + k2 = 0x42c0ffee; + } else { + k2 = config.bucketMatchers().hashCode(); + } return Long.rotateLeft(Integer.toUnsignedLong(k1), 32) + Integer.toUnsignedLong(k2); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java index 1ab7f710ca8..8fb50e81930 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsClient.java @@ -25,20 +25,26 @@ import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports; import io.envoyproxy.envoy.service.rate_limit_quota.v3.RateLimitQuotaUsageReports.BucketQuotaUsage; import io.grpc.Grpc; +import io.grpc.InternalLogId; import io.grpc.ManagedChannel; +import io.grpc.internal.GrpcUtil; import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.client.XdsLogger.XdsLogLevel; import io.grpc.xds.internal.rlqs.RlqsBucket.RlqsBucketUsage; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; +import javax.annotation.Nullable; public final class RlqsClient { - private static final Logger logger = Logger.getLogger(RlqsClient.class.getName()); + // TODO(sergiitk): [IMPL] remove + // Do do not fail on parsing errors, only log requests. + static final boolean dryRun = GrpcUtil.getFlag("GRPC_EXPERIMENTAL_RLQS_DRY_RUN", false); + + private final XdsLogger logger; private final RemoteServerInfo serverInfo; private final Consumer> bucketsUpdateCallback; @@ -46,10 +52,14 @@ public final class RlqsClient { RlqsClient( RemoteServerInfo serverInfo, String domain, - Consumer> bucketsUpdateCallback) { + Consumer> bucketsUpdateCallback, String prettyHash) { // TODO(sergiitk): [post] check not null. this.serverInfo = serverInfo; this.bucketsUpdateCallback = bucketsUpdateCallback; + + logger = XdsLogger.withLogId( + InternalLogId.allocate(this.getClass(), "<" + prettyHash + "> " + serverInfo.target())); + this.rlqsStream = new RlqsStream(serverInfo, domain); } @@ -62,7 +72,7 @@ public void sendUsageReports(List bucketUsages) { } public void shutdown() { - logger.log(Level.FINER, "Shutting down RlqsClient to {0}", serverInfo.target()); + logger.log(XdsLogLevel.DEBUG, "Shutting down RlqsClient to {0}", serverInfo.target()); // TODO(sergiitk): [IMPL] RlqsClient shutdown } @@ -72,18 +82,26 @@ public void handleStreamClosed() { private class RlqsStream { private final AtomicBoolean isFirstReport = new AtomicBoolean(true); - private final ManagedChannel channel; private final String domain; + @Nullable private final ClientCallStreamObserver clientCallStream; RlqsStream(RemoteServerInfo serverInfo, String domain) { this.domain = domain; - channel = Grpc.newChannelBuilder(serverInfo.target(), serverInfo.channelCredentials()) - .keepAliveTime(10, TimeUnit.SECONDS) - .keepAliveWithoutCalls(true) - .build(); - // keepalive? + + if (dryRun) { + clientCallStream = null; + logger.log(XdsLogLevel.DEBUG, "Dry run, not connecting to " + serverInfo.target()); + return; + } + // TODO(sergiitk): [IMPL] Manage State changes? + ManagedChannel channel = + Grpc.newChannelBuilder(serverInfo.target(), serverInfo.channelCredentials()).build(); + // keepalive? + // .keepAliveTime(10, TimeUnit.SECONDS) + // .keepAliveWithoutCalls(true) + RateLimitQuotaServiceStub stub = RateLimitQuotaServiceGrpc.newStub(channel); clientCallStream = (ClientCallStreamObserver) stub.streamRateLimitQuotas(new RlqsStreamObserver()); @@ -107,6 +125,10 @@ void reportUsage(List usageReports) { for (RlqsBucket.RlqsBucketUsage bucketUsage : usageReports) { report.addBucketQuotaUsages(toUsageReport(bucketUsage)); } + if (clientCallStream == null) { + logger.log(XdsLogLevel.DEBUG, "Dry run, skipping bucket usage report: " + report.build()); + return; + } clientCallStream.onNext(report.build()); } @@ -128,12 +150,12 @@ public void onNext(RateLimitQuotaResponse response) { @Override public void onError(Throwable t) { - + logger.log(XdsLogLevel.DEBUG, "Got error in RlqsStreamObserver: " + t.toString()); } @Override public void onCompleted() { - + logger.log(XdsLogLevel.DEBUG, "RlqsStreamObserver completed"); } } } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java index 70570fbd2b9..01d34f601d7 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java @@ -17,7 +17,10 @@ package io.grpc.xds.internal.rlqs; import com.google.common.collect.ImmutableList; +import io.grpc.InternalLogId; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.client.XdsLogger.XdsLogLevel; import io.grpc.xds.internal.datatype.RateLimitStrategy; import io.grpc.xds.internal.matchers.HttpMatchInput; import io.grpc.xds.internal.matchers.Matcher; @@ -29,11 +32,9 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; public class RlqsEngine { - private static final Logger logger = Logger.getLogger(RlqsEngine.class.getName()); + private final XdsLogger logger; private final RlqsClient rlqsClient; private final Matcher bucketMatchers; @@ -49,8 +50,14 @@ public RlqsEngine( this.bucketMatchers = bucketMatchers; this.configHash = configHash; this.scheduler = scheduler; + + String prettyHash = "0x" + Long.toHexString(configHash); + logger = XdsLogger.withLogId(InternalLogId.allocate(this.getClass(), prettyHash)); + logger.log(XdsLogLevel.DEBUG, + "Initialized RlqsEngine for hash={0}, domain={1}", prettyHash, domain); + bucketCache = new RlqsBucketCache(); - rlqsClient = new RlqsClient(rlqsServer, domain, this::onBucketsUpdate); + rlqsClient = new RlqsClient(rlqsServer, domain, this::onBucketsUpdate, prettyHash); } public RlqsRateLimitResult rateLimit(HttpMatchInput input) { @@ -95,7 +102,8 @@ private void scheduleImmediateReport(RlqsBucket newBucket) { 1, TimeUnit.MICROSECONDS); } catch (RejectedExecutionException e) { // Shouldn't happen. - logger.finer("Couldn't schedule immediate report for bucket " + newBucket.getBucketId()); + logger.log(XdsLogLevel.WARNING, + "Couldn't schedule immediate report for bucket " + newBucket.getBucketId()); } } @@ -123,7 +131,7 @@ private void reportBucketsWithInterval(long intervalMillis) { public void shutdown() { // TODO(sergiitk): [IMPL] Timers shutdown // TODO(sergiitk): [IMPL] RlqsEngine shutdown - logger.log(Level.FINER, "Shutting down RlqsEngine with hash {0}", configHash); + logger.log(XdsLogLevel.DEBUG, "Shutting down RlqsEngine with hash {0}", configHash); rlqsClient.shutdown(); } } From 877dfc084d6e94de652b1ac90022e2a5b75dfe3a Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 17 Oct 2024 15:24:27 -0700 Subject: [PATCH 42/47] RlqsEngine -> RlqsFilterState --- xds/src/main/java/io/grpc/xds/RlqsFilter.java | 6 +- .../io/grpc/xds/internal/rlqs/RlqsCache.java | 64 ++++++++++--------- .../{RlqsEngine.java => RlqsFilterState.java} | 10 +-- 3 files changed, 42 insertions(+), 38 deletions(-) rename xds/src/main/java/io/grpc/xds/internal/rlqs/{RlqsEngine.java => RlqsFilterState.java} (94%) diff --git a/xds/src/main/java/io/grpc/xds/RlqsFilter.java b/xds/src/main/java/io/grpc/xds/RlqsFilter.java index e6d06eb9bca..6fb27cd5a5d 100644 --- a/xds/src/main/java/io/grpc/xds/RlqsFilter.java +++ b/xds/src/main/java/io/grpc/xds/RlqsFilter.java @@ -44,7 +44,7 @@ import io.grpc.xds.internal.matchers.OnMatch; import io.grpc.xds.internal.rlqs.RlqsBucketSettings; import io.grpc.xds.internal.rlqs.RlqsCache; -import io.grpc.xds.internal.rlqs.RlqsEngine; +import io.grpc.xds.internal.rlqs.RlqsFilterState; import io.grpc.xds.internal.rlqs.RlqsRateLimitResult; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -168,7 +168,7 @@ private ServerInterceptor generateRlqsInterceptor(RlqsFilterConfig config) { return null; } - final RlqsEngine rlqsEngine = rlqsCache.getOrCreateRlqsEngine(config); + final RlqsFilterState rlqsFilterState = rlqsCache.getOrCreateFilterState(config); return new ServerInterceptor() { @Override @@ -182,7 +182,7 @@ public Listener interceptCall( return next.startCall(call, headers); } - RlqsRateLimitResult result = rlqsEngine.rateLimit(httpMatchInput); + RlqsRateLimitResult result = rlqsFilterState.rateLimit(httpMatchInput); if (result.isAllowed()) { return next.startCall(call, headers); } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java index 8b7fa1c8f35..f2da5da6b61 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsCache.java @@ -18,37 +18,42 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.collect.Sets; +import com.google.common.base.Throwables; import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; +import io.grpc.InternalLogId; import io.grpc.SynchronizationContext; import io.grpc.xds.RlqsFilterConfig; import io.grpc.xds.client.Bootstrapper.RemoteServerInfo; +import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.client.XdsLogger.XdsLogLevel; import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; -import java.util.logging.Level; -import java.util.logging.Logger; public final class RlqsCache { - private static final Logger logger = Logger.getLogger(RlqsCache.class.getName()); - // TODO(sergiitk): [QUESTION] always in sync context? private volatile boolean shutdown = false; - private final SynchronizationContext syncContext = new SynchronizationContext((thread, error) -> { - String message = "Uncaught exception in RlqsCache SynchronizationContext. Panic!"; - logger.log(Level.FINE, message, error); - throw new RlqsPoolSynchronizationException(message, error); - }); - - private final ConcurrentHashMap enginePool = new ConcurrentHashMap<>(); - Set enginesToShutdown = Sets.newConcurrentHashSet(); + + private final XdsLogger logger; + private final SynchronizationContext syncContext; + + private final ConcurrentMap filterStateCache = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler; private RlqsCache(ScheduledExecutorService scheduler) { this.scheduler = checkNotNull(scheduler, "scheduler"); + // TODO(sergiitk): should be filter name? + logger = XdsLogger.withLogId(InternalLogId.allocate(this.getClass(), null)); + + syncContext = new SynchronizationContext((thread, error) -> { + String message = "Uncaught exception in RlqsCache SynchronizationContext. Panic!"; + logger.log(XdsLogLevel.DEBUG, + message + " {0} \nTrace:\n {1}", error, Throwables.getStackTraceAsString(error)); + throw new RlqsCacheSynchronizationException(message, error); + }); } /** Creates an instance. */ @@ -64,29 +69,30 @@ public void shutdown() { } syncContext.execute(() -> { shutdown = true; - logger.log(Level.FINER, "Shutting down RlqsCache"); - enginesToShutdown.clear(); - for (long configHash : enginePool.keySet()) { - enginePool.get(configHash).shutdown(); + logger.log(XdsLogLevel.DEBUG, "Shutting down RlqsCache"); + for (long configHash : filterStateCache.keySet()) { + filterStateCache.get(configHash).shutdown(); } - enginePool.clear(); + filterStateCache.clear(); shutdown = false; }); } - public void shutdownRlqsEngine(RlqsFilterConfig oldConfig) { + public void shutdownFilterState(RlqsFilterConfig oldConfig) { // TODO(sergiitk): shutdown one + // make it async. } - public RlqsEngine getOrCreateRlqsEngine(final RlqsFilterConfig config) { - long configHash = hashRlqsFilterConfig(config); - return enginePool.computeIfAbsent(configHash, k -> newRlqsEngine(k, config)); + public RlqsFilterState getOrCreateFilterState(final RlqsFilterConfig config) { + // TODO(sergiitk): handle being shut down. + long configHash = hashFilterConfig(config); + return filterStateCache.computeIfAbsent(configHash, k -> newFilterState(k, config)); } - private RlqsEngine newRlqsEngine(long configHash, RlqsFilterConfig config) { + private RlqsFilterState newFilterState(long configHash, RlqsFilterConfig config) { // TODO(sergiitk): [IMPL] get channel creds from the bootstrap. ChannelCredentials creds = InsecureChannelCredentials.create(); - return new RlqsEngine( + return new RlqsFilterState( RemoteServerInfo.create(config.rlqsService().targetUri(), creds), config.domain(), config.bucketMatchers(), @@ -94,7 +100,7 @@ private RlqsEngine newRlqsEngine(long configHash, RlqsFilterConfig config) { scheduler); } - private long hashRlqsFilterConfig(RlqsFilterConfig config) { + private long hashFilterConfig(RlqsFilterConfig config) { // TODO(sergiitk): [QUESTION] better name? - ask Eric. // TODO(sergiitk): [DESIGN] the key should be hashed (domain + buckets) merged config? // TODO(sergiitk): [IMPL] Hash buckets @@ -111,13 +117,11 @@ private long hashRlqsFilterConfig(RlqsFilterConfig config) { /** * Throws when fail to bootstrap or initialize the XdsClient. */ - public static final class RlqsPoolSynchronizationException extends RuntimeException { + public static final class RlqsCacheSynchronizationException extends RuntimeException { private static final long serialVersionUID = 1L; - public RlqsPoolSynchronizationException(String message, Throwable cause) { + public RlqsCacheSynchronizationException(String message, Throwable cause) { super(message, cause); } } - - } diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsFilterState.java similarity index 94% rename from xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java rename to xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsFilterState.java index 01d34f601d7..923153d2ec0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsFilterState.java @@ -33,7 +33,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class RlqsEngine { +public class RlqsFilterState { private final XdsLogger logger; private final RlqsClient rlqsClient; @@ -43,7 +43,7 @@ public class RlqsEngine { private final ScheduledExecutorService scheduler; private final ConcurrentMap> timers = new ConcurrentHashMap<>(); - public RlqsEngine( + public RlqsFilterState( RemoteServerInfo rlqsServer, String domain, Matcher bucketMatchers, long configHash, ScheduledExecutorService scheduler) { @@ -54,7 +54,7 @@ public RlqsEngine( String prettyHash = "0x" + Long.toHexString(configHash); logger = XdsLogger.withLogId(InternalLogId.allocate(this.getClass(), prettyHash)); logger.log(XdsLogLevel.DEBUG, - "Initialized RlqsEngine for hash={0}, domain={1}", prettyHash, domain); + "Initialized RlqsFilterState for hash={0}, domain={1}", prettyHash, domain); bucketCache = new RlqsBucketCache(); rlqsClient = new RlqsClient(rlqsServer, domain, this::onBucketsUpdate, prettyHash); @@ -130,8 +130,8 @@ private void reportBucketsWithInterval(long intervalMillis) { public void shutdown() { // TODO(sergiitk): [IMPL] Timers shutdown - // TODO(sergiitk): [IMPL] RlqsEngine shutdown - logger.log(XdsLogLevel.DEBUG, "Shutting down RlqsEngine with hash {0}", configHash); + // TODO(sergiitk): [IMPL] RlqsFilterState shutdown + logger.log(XdsLogLevel.DEBUG, "Shutting down RlqsFilterState with hash {0}", configHash); rlqsClient.shutdown(); } } From 921a502d02acb7adbf709518cf722e5a8936aed4 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 21 Oct 2024 16:34:50 -0700 Subject: [PATCH 43/47] Add CEL types to the message printer --- xds/src/main/java/io/grpc/xds/MessagePrinter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/MessagePrinter.java b/xds/src/main/java/io/grpc/xds/MessagePrinter.java index ea45cdb7c3a..f94c775776d 100644 --- a/xds/src/main/java/io/grpc/xds/MessagePrinter.java +++ b/xds/src/main/java/io/grpc/xds/MessagePrinter.java @@ -16,6 +16,8 @@ package io.grpc.xds; +import com.github.xds.type.matcher.v3.CelMatcher; +import com.github.xds.type.matcher.v3.HttpAttributesCelMatchInput; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -63,6 +65,8 @@ private static JsonFormat.Printer newPrinter() { // RLQS .add(RateLimitQuotaFilterConfig.getDescriptor()) .add(RateLimitQuotaOverride.getDescriptor()) + .add(HttpAttributesCelMatchInput.getDescriptor()) + .add(CelMatcher.getDescriptor()) // UpstreamTlsContext and DownstreamTlsContext in v3 are not transitively imported // by top-level resource types. .add(UpstreamTlsContext.getDescriptor()) From 7af3fccd5bf261bb0fa0b97d7bfa5a400b844e52 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Wed, 23 Oct 2024 17:18:54 -0700 Subject: [PATCH 44/47] Add CEL macro verifications --- .../xds/internal/matchers/CelMatcherTest.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java index 782ce57724f..2014218ce46 100644 --- a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java @@ -17,18 +17,25 @@ package io.grpc.xds.internal.matchers; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelValidationException; import dev.cel.common.CelValidationResult; import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; import dev.cel.runtime.CelEvaluationException; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; import io.grpc.NoopServerCall; import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; import io.grpc.StringMarshaller; import org.junit.Before; import org.junit.Test; @@ -43,9 +50,10 @@ public class CelMatcherTest { CelCompilerFactory.standardCelCompilerBuilder() .addVar("request", SimpleType.ANY) .setResultType(SimpleType.BOOL) + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); private static final CelValidationResult celProg1 = - CEL_COMPILER.compile("request.method == \"POST\""); + CEL_COMPILER.compile("request.method == 'POST'"); CelAbstractSyntaxTree ast1; CelMatcher matcher1; @@ -92,4 +100,34 @@ public void construct() throws CelEvaluationException { public void testProgTrue() { assertThat(matcher1.test(fakeInput)).isTrue(); } + + @Test + public void macros_comprehensionsDisabled() throws Exception { + CelMatcher matcherWithComprehensions = newMatcher( + "size(['foo', 'bar'].map(x, [request.headers[x], request.headers[x]])) == 1"); + + Status status = assertThrows(StatusRuntimeException.class, + () -> matcherWithComprehensions.test(fakeInput)).getStatus(); + + assertThat(status.getCode()).isEqualTo(Code.UNKNOWN); + assertThat(status.getCause()).isInstanceOf(CelEvaluationException.class); + + // Verify CelErrorCode is ITERATION_BUDGET_EXCEEDED. + CelEvaluationException cause = (CelEvaluationException) status.getCause(); + assertThat(cause.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED); + } + + @Test + public void macros_hasEnabled() throws Exception { + boolean result = newMatcher("has(request.path)").test(fakeInput); + assertThat(result).isTrue(); + } + + private CelMatcher newMatcher(String expr) throws CelValidationException, CelEvaluationException { + return CelMatcher.create(celAst(expr)); + } + + private CelAbstractSyntaxTree celAst(String expr) throws CelValidationException { + return CEL_COMPILER.compile(expr).getAst(); + } } From e56c032a8b56c068d26dbf895370ee356b1cd26c Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Fri, 25 Oct 2024 12:02:36 -0700 Subject: [PATCH 45/47] Add CelMatcher.fromEnvoyProto - just to import dev.cel.expr --- .../xds/internal/matchers/CelMatcher.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java index 8659d6845ae..186cc64b439 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java @@ -19,7 +19,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.expr.CheckedExpr; import dev.cel.runtime.CelEvaluationException; +import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; import java.util.function.Predicate; /** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ @@ -42,6 +45,31 @@ public static CelMatcher create(CelAbstractSyntaxTree ast, String description) return new CelMatcher(ast, description); } + public static CelMatcher fromEnvoyProto(com.github.xds.type.matcher.v3.CelMatcher proto) + throws ResourceInvalidException { + com.github.xds.type.v3.CelExpression exprMatch = proto.getExprMatch(); + // TODO(sergiitk): do i need this? + // checkNotNull(exprMatch); + + if (!exprMatch.hasCelExprChecked()) { + throw ResourceInvalidException.ofResource(proto, "cel_expr_checked is required"); + } + + // Canonical CEL. + CheckedExpr celExprChecked = exprMatch.getCelExprChecked(); + + // TODO(sergiitk): catch tree build errors? + CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromCheckedExpr(celExprChecked).getAst(); + + try { + return new CelMatcher(ast, proto.getDescription()); + } catch (CelEvaluationException e) { + throw ResourceInvalidException.ofResource(exprMatch, + "Error Building CEL Program cel_expr_checked: " + e.getErrorCode() + " " + + e.getMessage()); + } + } + public String description() { return description; } From 29f6fe72e5a051a6b24e611443874fe8bb45f128 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Thu, 31 Oct 2024 11:21:15 -0700 Subject: [PATCH 46/47] LongAdder note --- xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java index 25a93892932..37170477dd0 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java +++ b/xds/src/main/java/io/grpc/xds/internal/rlqs/RlqsBucket.java @@ -34,6 +34,8 @@ public class RlqsBucket { // TODO(sergiitk): [impl] consider AtomicLongFieldUpdater private final AtomicLong lastSnapshotTimeNanos = new AtomicLong(-1); + + // TODO(sergiitk): [impl] consider java.util.concurrent.atomic.LongAdder for counters private final AtomicLong numRequestsAllowed = new AtomicLong(); private final AtomicLong numRequestsDenied = new AtomicLong(); From 065645970506e3b88128f4bd2a32e76cda74b56e Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Mon, 28 Oct 2024 16:33:14 -0700 Subject: [PATCH 47/47] CEL variable resolver --- .../io/grpc/xds/internal/MetadataHelper.java | 4 + .../xds/internal/matchers/CelMatcher.java | 6 +- .../internal/matchers/GrpcCelEnvironment.java | 81 +++++++-- .../xds/internal/matchers/HeadersWrapper.java | 160 ++++++++++++++++++ .../xds/internal/matchers/HttpMatchInput.java | 33 +++- .../xds/internal/matchers/CelMatcherTest.java | 69 +++++--- 6 files changed, 309 insertions(+), 44 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/matchers/HeadersWrapper.java diff --git a/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java b/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java index eed684fe255..eb3589835cc 100644 --- a/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java +++ b/xds/src/main/java/io/grpc/xds/internal/MetadataHelper.java @@ -60,4 +60,8 @@ public static String deserializeHeader(Metadata metadata, String headerName) { Iterable values = metadata.getAll(key); return values == null ? null : String.join(",", values); } + + public static boolean containsHeader(Metadata metadata, String headerName) { + return metadata.containsKey(Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER)); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java index 186cc64b439..803a88696a6 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/CelMatcher.java @@ -76,10 +76,6 @@ public String description() { @Override public boolean test(HttpMatchInput httpMatchInput) { - // if (httpMatchInput.headers().keys().isEmpty()) { - // return false; - // } - // TODO(sergiitk): [IMPL] convert headers to cel args - return program.eval(httpMatchInput.serverCall(), httpMatchInput.headers()); + return program.eval(httpMatchInput); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java b/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java index ae67f5bbfcd..aad917a5130 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/GrpcCelEnvironment.java @@ -16,24 +16,31 @@ package io.grpc.xds.internal.matchers; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; +import com.google.common.base.Splitter; import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; +import dev.cel.common.CelRuntimeException; import dev.cel.common.types.SimpleType; import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelRuntime; import dev.cel.runtime.CelRuntimeFactory; -import io.grpc.Metadata; -import io.grpc.ServerCall; +import dev.cel.runtime.CelVariableResolver; import io.grpc.Status; -import io.grpc.xds.internal.MetadataHelper; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; /** Unified Matcher API: xds.type.matcher.v3.CelMatcher. */ public class GrpcCelEnvironment { + private static final CelOptions CEL_OPTIONS = CelOptions + .current() + .comprehensionMaxIterations(0) + .resolveTypeDependencies(false) + .build(); private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory .standardCelRuntimeBuilder() - .setOptions(CelOptions.current().comprehensionMaxIterations(0).build()) + .setOptions(CEL_OPTIONS) .build(); private final CelRuntime.Program program; @@ -45,17 +52,61 @@ public class GrpcCelEnvironment { this.program = CEL_RUNTIME.createProgram(ast); } - public boolean eval(ServerCall serverCall, Metadata metadata) { - ImmutableMap.Builder requestBuilder = ImmutableMap.builder() - .put("method", "POST") - .put("host", Strings.nullToEmpty(serverCall.getAuthority())) - .put("path", "/" + serverCall.getMethodDescriptor().getFullMethodName()) - .put("headers", MetadataHelper.metadataToHeaders(metadata)); - // TODO(sergiitk): handle other pseudo-headers + public boolean eval(HttpMatchInput httpMatchInput) { try { - return (boolean) program.eval(ImmutableMap.of("request", requestBuilder.build())); - } catch (CelEvaluationException e) { + GrpcCelVariableResolver requestResolver = new GrpcCelVariableResolver(httpMatchInput); + return (boolean) program.eval(requestResolver); + } catch (CelEvaluationException | ClassCastException e) { throw Status.fromThrowable(e).asRuntimeException(); } } + + static class GrpcCelVariableResolver implements CelVariableResolver { + private static final Splitter SPLITTER = Splitter.on('.').limit(2); + private final HttpMatchInput httpMatchInput; + + GrpcCelVariableResolver(HttpMatchInput httpMatchInput) { + this.httpMatchInput = httpMatchInput; + } + + @Override + public Optional find(String name) { + List components = SPLITTER.splitToList(name); + if (components.size() < 2 || !components.get(0).equals("request")) { + return Optional.empty(); + } + return Optional.ofNullable(getRequestField(components.get(1))); + } + + @Nullable + private Object getRequestField(String requestField) { + switch (requestField) { + case "headers": + return httpMatchInput.getHeadersWrapper(); + case "host": + return httpMatchInput.getHost(); + case "id": + return httpMatchInput.getHeadersWrapper().get("x-request-id"); + case "method": + return httpMatchInput.getMethod(); + case "path": + case "url_path": + return httpMatchInput.getPath(); + case "query": + return ""; + case "referer": + return httpMatchInput.getHeadersWrapper().get("referer"); + case "useragent": + return httpMatchInput.getHeadersWrapper().get("user-agent"); + default: + // Throwing instead of Optional.empty() prevents evaluation non-boolean result type + // when comparing unknown fields, f.e. `request.protocol == 'HTTP'` will silently + // fail because `null == "HTTP" is not a valid CEL operation. + throw new CelRuntimeException( + // Similar to dev.cel.runtime.DescriptorMessageProvider#selectField + new IllegalArgumentException("request." + requestField), + CelErrorCode.ATTRIBUTE_NOT_FOUND); + } + } + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/HeadersWrapper.java b/xds/src/main/java/io/grpc/xds/internal/matchers/HeadersWrapper.java new file mode 100644 index 00000000000..a92b64289b2 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/HeadersWrapper.java @@ -0,0 +1,160 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds.internal.matchers; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.DoNotCall; +import io.grpc.xds.internal.MetadataHelper; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nullable; + +public final class HeadersWrapper extends AbstractMap { + private static final ImmutableSet PSEUDO_HEADERS = + ImmutableSet.of(":method", ":authority", ":path"); + private final HttpMatchInput httpMatchInput; + + HeadersWrapper(HttpMatchInput httpMatchInput) { + this.httpMatchInput = httpMatchInput; + } + + @Override + @Nullable + public String get(Object key) { + String headerName = (String) key; + // Pseudo-headers. + switch (headerName) { + case ":method": + return httpMatchInput.getMethod(); + case ":authority": + return httpMatchInput.getHost(); + case ":path": + return httpMatchInput.getPath(); + default: + return MetadataHelper.deserializeHeader(httpMatchInput.metadata(), headerName); + } + } + + @Override + public String getOrDefault(Object key, String defaultValue) { + String value = get(key); + return value != null ? value : defaultValue; + } + + @Override + public boolean containsKey(Object key) { + String headerName = (String) key; + if (PSEUDO_HEADERS.contains(headerName)) { + return true; + } + return MetadataHelper.containsHeader(httpMatchInput.metadata(), headerName); + } + + @Override + public Set keySet() { + return ImmutableSet.builder() + .addAll(httpMatchInput.metadata().keys()) + .addAll(PSEUDO_HEADERS).build(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public Set> entrySet() { + throw new UnsupportedOperationException( + "Should not be called to prevent resolving header values."); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public Collection values() { + throw new UnsupportedOperationException( + "Should not be called to prevent resolving header values."); + } + + @Override + public String toString() { + // Prevent iterating to avoid resolving all values on "key not found". + return getClass().getName() + "@" + Integer.toHexString(hashCode()); + } + + @Override public int hashCode() { + return Objects.hashCode(httpMatchInput.serverCall(), httpMatchInput.metadata()); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public void replaceAll(BiFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String putIfAbsent(String key, String value) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public boolean remove(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public boolean replace(String key, String oldValue, String newValue) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String replace(String key, String value) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String computeIfAbsent( + String key, Function mappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String computeIfPresent( + String key, BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String compute( + String key, BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + + @Override + @DoNotCall("Always throws UnsupportedOperationException") + public String merge( + String key, String value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java index 04dd9cbe4e5..5c960cb55ba 100644 --- a/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java +++ b/xds/src/main/java/io/grpc/xds/internal/matchers/HttpMatchInput.java @@ -18,17 +18,44 @@ package io.grpc.xds.internal.matchers; import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.base.Strings; import io.grpc.Metadata; import io.grpc.ServerCall; +import io.grpc.xds.internal.MetadataHelper; +import java.util.Map; +import javax.annotation.Nullable; @AutoValue public abstract class HttpMatchInput { - public abstract Metadata headers(); + public abstract Metadata metadata(); // TODO(sergiitk): [IMPL] consider public abstract ServerCall serverCall(); - public static HttpMatchInput create(Metadata headers, ServerCall serverCall) { - return new AutoValue_HttpMatchInput(headers, serverCall); + public static HttpMatchInput create(Metadata metadata, ServerCall serverCall) { + return new AutoValue_HttpMatchInput(metadata, serverCall); + } + + public String getMethod() { + return "POST"; + } + + public String getHost() { + return Strings.nullToEmpty(serverCall().getAuthority()); + } + + public String getPath() { + return "/" + serverCall().getMethodDescriptor().getFullMethodName(); + } + + @Nullable + public String getHeader(String headerName) { + return MetadataHelper.deserializeHeader(metadata(), headerName); + } + + @Memoized + public Map getHeadersWrapper() { + return new HeadersWrapper(this); } } diff --git a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java index 2014218ce46..130374cbe90 100644 --- a/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/matchers/CelMatcherTest.java @@ -22,7 +22,7 @@ import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelErrorCode; import dev.cel.common.CelValidationException; -import dev.cel.common.CelValidationResult; +import dev.cel.common.types.MapType; import dev.cel.common.types.SimpleType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerFactory; @@ -37,7 +37,6 @@ import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.StringMarshaller; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -48,19 +47,21 @@ public class CelMatcherTest { // These instances are immutable and thus trivially thread-safe and amenable to caching. private static final CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder() - .addVar("request", SimpleType.ANY) + .addVar("request.path", SimpleType.STRING) + .addVar("request.host", SimpleType.STRING) + .addVar("request.method", SimpleType.STRING) + .addVar("request.headers", MapType.create(SimpleType.STRING, SimpleType.STRING)) + // request.protocol is a legal input, but we don't set it in java. + // TODO(sergiitk): add other fields not supported by gRPC + .addVar("request.protocol", SimpleType.STRING) .setResultType(SimpleType.BOOL) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); - private static final CelValidationResult celProg1 = - CEL_COMPILER.compile("request.method == 'POST'"); - CelAbstractSyntaxTree ast1; - CelMatcher matcher1; private static final HttpMatchInput fakeInput = new HttpMatchInput() { @Override - public Metadata headers() { + public Metadata metadata() { return new Metadata(); } @@ -81,24 +82,50 @@ public MethodDescriptor getMethodDescriptor() { } }; - @Before - public void setUp() throws Exception { - ast1 = celProg1.getAst(); - matcher1 = CelMatcher.create(ast1); - } - @Test - public void construct() throws CelEvaluationException { - assertThat(matcher1.description()).isEqualTo(""); + public void construct() throws Exception { + CelAbstractSyntaxTree ast = celAst("1 == 1"); + CelMatcher matcher = CelMatcher.create(ast); + assertThat(matcher.description()).isEqualTo(""); String description = "Optional description"; - CelMatcher matcher = CelMatcher.create(ast1, description); + matcher = CelMatcher.create(ast, description); assertThat(matcher.description()).isEqualTo(description); } @Test - public void testProgTrue() { - assertThat(matcher1.test(fakeInput)).isTrue(); + public void progTrue() throws Exception { + assertThat(newMatcher("request.method == 'POST'").test(fakeInput)).isTrue(); + } + + @Test + public void unknownRequestProperty() throws Exception { + CelMatcher matcher = newMatcher("request.protocol == 'Whatever'"); + + Status status = assertThrows(StatusRuntimeException.class, + () -> matcher.test(fakeInput)).getStatus(); + + assertThat(status.getCode()).isEqualTo(Code.UNKNOWN); + assertThat(status.getCause()).isInstanceOf(CelEvaluationException.class); + + // Verify CelErrorCode is ATTRIBUTE_NOT_FOUND. + CelEvaluationException cause = (CelEvaluationException) status.getCause(); + assertThat(cause.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); + } + + @Test + public void unknownHeader() throws Exception { + CelMatcher matcher = newMatcher("request.headers['foo'] == 'bar'"); + + Status status = assertThrows(StatusRuntimeException.class, + () -> matcher.test(fakeInput)).getStatus(); + + assertThat(status.getCode()).isEqualTo(Code.UNKNOWN); + assertThat(status.getCause()).isInstanceOf(CelEvaluationException.class); + + // Verify CelErrorCode is ATTRIBUTE_NOT_FOUND. + CelEvaluationException cause = (CelEvaluationException) status.getCause(); + assertThat(cause.getErrorCode()).isEqualTo(CelErrorCode.ATTRIBUTE_NOT_FOUND); } @Test @@ -119,8 +146,8 @@ public void macros_comprehensionsDisabled() throws Exception { @Test public void macros_hasEnabled() throws Exception { - boolean result = newMatcher("has(request.path)").test(fakeInput); - assertThat(result).isTrue(); + boolean result = newMatcher("has(request.headers.foo)").test(fakeInput); + assertThat(result).isFalse(); } private CelMatcher newMatcher(String expr) throws CelValidationException, CelEvaluationException {