From 74081be822f5d6936eb2d70d65940bffadb5a330 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Wed, 14 May 2025 16:29:00 -0300 Subject: [PATCH 1/5] Allow realm context resolution to execute blocking calls --- .../quarkus/config/QuarkusProducers.java | 2 +- .../quarkus}/context/RealmContextFilter.java | 42 +++++++++---------- .../logging/QuarkusLoggingMDCFilter.java | 2 +- .../quarkus/tracing/QuarkusTracingFilter.java | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) rename {service/common/src/main/java/org/apache/polaris/service => quarkus/service/src/main/java/org/apache/polaris/service/quarkus}/context/RealmContextFilter.java (50%) diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java index 89bf2c6fb3..40b79980dc 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/config/QuarkusProducers.java @@ -60,7 +60,6 @@ import org.apache.polaris.service.catalog.io.FileIOFactory; import org.apache.polaris.service.config.RealmEntityManagerFactory; import org.apache.polaris.service.context.RealmContextConfiguration; -import org.apache.polaris.service.context.RealmContextFilter; import org.apache.polaris.service.context.RealmContextResolver; import org.apache.polaris.service.events.PolarisEventListener; import org.apache.polaris.service.quarkus.auth.QuarkusAuthenticationConfiguration; @@ -68,6 +67,7 @@ import org.apache.polaris.service.quarkus.auth.external.tenant.OidcTenantResolver; import org.apache.polaris.service.quarkus.catalog.io.QuarkusFileIOConfiguration; import org.apache.polaris.service.quarkus.context.QuarkusRealmContextConfiguration; +import org.apache.polaris.service.quarkus.context.RealmContextFilter; import org.apache.polaris.service.quarkus.events.QuarkusPolarisEventListenerConfiguration; import org.apache.polaris.service.quarkus.persistence.QuarkusPersistenceConfiguration; import org.apache.polaris.service.quarkus.ratelimiter.QuarkusRateLimiterFilterConfiguration; diff --git a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java similarity index 50% rename from service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java rename to quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java index 7939d05b1c..a08d5de3bf 100644 --- a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextFilter.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java @@ -16,36 +16,36 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service.context; +package org.apache.polaris.service.quarkus.context; -import jakarta.annotation.Priority; -import jakarta.enterprise.context.ApplicationScoped; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.Vertx; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.PreMatching; -import jakarta.ws.rs.ext.Provider; -import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.service.config.PolarisFilterPriorities; +import org.apache.polaris.service.context.RealmContextResolver; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; -@PreMatching -@ApplicationScoped -@Priority(PolarisFilterPriorities.REALM_CONTEXT_FILTER) -@Provider -public class RealmContextFilter implements ContainerRequestFilter { +public class RealmContextFilter { public static final String REALM_CONTEXT_KEY = "realmContext"; @Inject RealmContextResolver realmContextResolver; + @Inject Vertx vertx; - @Override - public void filter(ContainerRequestContext rc) { - RealmContext realmContext = - realmContextResolver.resolveRealmContext( - rc.getUriInfo().getRequestUri().toString(), - rc.getMethod(), - rc.getUriInfo().getPath(), - rc.getHeaders()::getFirst); - rc.setProperty(REALM_CONTEXT_KEY, realmContext); + @ServerRequestFilter(preMatching = true, priority = PolarisFilterPriorities.REALM_CONTEXT_FILTER) + public Uni resolveRealmContext(ContainerRequestContext rc) { + // Note: the default implementation of RealmContextResolver does not block, + // but since other implementations may, we need to use executeBlocking(). + return vertx + .executeBlocking( + () -> + realmContextResolver.resolveRealmContext( + rc.getUriInfo().getRequestUri().toString(), + rc.getMethod(), + rc.getUriInfo().getPath(), + rc.getHeaders()::getFirst)) + .invoke(realmContext -> rc.setProperty(REALM_CONTEXT_KEY, realmContext)) + .replaceWithVoid(); } } diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java index 3062fabd30..cc4e7cd452 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/logging/QuarkusLoggingMDCFilter.java @@ -18,7 +18,7 @@ */ package org.apache.polaris.service.quarkus.logging; -import static org.apache.polaris.service.context.RealmContextFilter.REALM_CONTEXT_KEY; +import static org.apache.polaris.service.quarkus.context.RealmContextFilter.REALM_CONTEXT_KEY; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java index 6035cceb4d..847905a6bd 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/tracing/QuarkusTracingFilter.java @@ -26,8 +26,8 @@ import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.ext.Provider; import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.service.context.RealmContextFilter; import org.apache.polaris.service.quarkus.config.QuarkusFilterPriorities; +import org.apache.polaris.service.quarkus.context.RealmContextFilter; import org.apache.polaris.service.quarkus.logging.QuarkusLoggingMDCFilter; import org.eclipse.microprofile.config.inject.ConfigProperty; From a2657ec2140fe6a9fbce5eeb1518e51cec464ba2 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Wed, 14 May 2025 18:12:40 -0300 Subject: [PATCH 2/5] improved design --- .../quarkus/context/RealmContextFilter.java | 30 +++++++++--- .../metrics/RealmIdTagContributor.java | 17 +++++-- .../quarkus/admin/RealmHeaderTest.java | 2 +- .../test/PolarisIntegrationTestFixture.java | 7 ++- .../context/DefaultRealmContextResolver.java | 16 ++++-- .../service/context/RealmContextResolver.java | 33 +++++++++++-- .../context/TestRealmContextResolver.java | 6 ++- .../UnresolvableRealmContextException.java | 34 ------------- .../exception/PolarisExceptionMapper.java | 3 -- .../context/DefaultRealmIdResolverTest.java | 49 +++++++++++++------ 10 files changed, 120 insertions(+), 77 deletions(-) delete mode 100644 service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java index a08d5de3bf..474e768987 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java @@ -19,9 +19,11 @@ package org.apache.polaris.service.quarkus.context; import io.smallrye.mutiny.Uni; -import io.vertx.mutiny.core.Vertx; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.iceberg.rest.responses.ErrorResponse; import org.apache.polaris.service.config.PolarisFilterPriorities; import org.apache.polaris.service.context.RealmContextResolver; import org.jboss.resteasy.reactive.server.ServerRequestFilter; @@ -31,21 +33,33 @@ public class RealmContextFilter { public static final String REALM_CONTEXT_KEY = "realmContext"; @Inject RealmContextResolver realmContextResolver; - @Inject Vertx vertx; @ServerRequestFilter(preMatching = true, priority = PolarisFilterPriorities.REALM_CONTEXT_FILTER) - public Uni resolveRealmContext(ContainerRequestContext rc) { - // Note: the default implementation of RealmContextResolver does not block, - // but since other implementations may, we need to use executeBlocking(). - return vertx - .executeBlocking( + public Uni resolveRealmContext(ContainerRequestContext rc) { + return Uni.createFrom() + .completionStage( () -> realmContextResolver.resolveRealmContext( rc.getUriInfo().getRequestUri().toString(), rc.getMethod(), rc.getUriInfo().getPath(), rc.getHeaders()::getFirst)) + .onItem() .invoke(realmContext -> rc.setProperty(REALM_CONTEXT_KEY, realmContext)) - .replaceWithVoid(); + .onItemOrFailure() + .transform((realmContext, error) -> error == null ? null : errorResponse(error)); + } + + private static Response errorResponse(Throwable error) { + return Response.status(Response.Status.NOT_FOUND) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity( + ErrorResponse.builder() + .responseCode(Response.Status.NOT_FOUND.getStatusCode()) + .withMessage( + error.getMessage() != null ? error.getMessage() : "Missing or invalid realm") + .withType("MissingOrInvalidRealm") + .build()) + .build(); } } diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java index 8388be9eba..d7ffa0e469 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java @@ -21,6 +21,7 @@ import io.micrometer.core.instrument.Tags; import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.vertx.core.http.HttpServerRequest; +import jakarta.annotation.Nullable; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.polaris.core.context.RealmContext; @@ -39,15 +40,25 @@ public Tags contribute(Context context) { HttpServerRequest request = context.request(); try { RealmContext realmContext = resolveRealmContext(request); - return Tags.of(TAG_REALM, realmContext.getRealmIdentifier()); + return realmContext == null + ? Tags.empty() + : Tags.of(TAG_REALM, realmContext.getRealmIdentifier()); } catch (Exception ignored) { // ignore, the RealmContextFilter will handle the error return Tags.empty(); } } + @Nullable private RealmContext resolveRealmContext(HttpServerRequest request) { - return realmContextResolver.resolveRealmContext( - request.absoluteURI(), request.method().name(), request.path(), request.headers()::get); + return realmContextResolver + .resolveRealmContext( + request.absoluteURI(), request.method().name(), request.path(), request.headers()::get) + .exceptionally(error -> null) + .toCompletableFuture() + // get the result of the CompletableFuture if it's already completed, + // otherwise return null as this code is executed on an event loop thread, + // and we don't want to block it. + .getNow(null); } } diff --git a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/RealmHeaderTest.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/RealmHeaderTest.java index f062f92639..eff5be116c 100644 --- a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/RealmHeaderTest.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/RealmHeaderTest.java @@ -90,7 +90,7 @@ public void testInvalidRealmHeaderValue() { .extracting(ErrorResponse::code, ErrorResponse::type, ErrorResponse::message) .containsExactly( Response.Status.NOT_FOUND.getStatusCode(), - "UnresolvableRealmContextException", + "MissingOrInvalidRealm", "Unknown realm: INVALID"); } } diff --git a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java index a550c98d1b..259cfc648f 100644 --- a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java @@ -104,8 +104,11 @@ private PolarisPrincipalSecrets fetchAdminSecrets() { } RealmContext realmContext = - helper.realmContextResolver.resolveRealmContext( - baseUri.toString(), "GET", "/", Map.of(REALM_PROPERTY_KEY, realm)); + helper + .realmContextResolver + .resolveRealmContext(baseUri.toString(), "GET", "/", Map.of(REALM_PROPERTY_KEY, realm)) + .toCompletableFuture() + .join(); BasePersistence metaStoreSession = helper.metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(); diff --git a/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java b/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java index 3318bd2f53..7d779ef85e 100644 --- a/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java +++ b/service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java @@ -21,6 +21,8 @@ import io.smallrye.common.annotation.Identifier; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.function.Function; import org.apache.polaris.core.context.RealmContext; @@ -36,21 +38,25 @@ public DefaultRealmContextResolver(RealmContextConfiguration configuration) { } @Override - public RealmContext resolveRealmContext( + public CompletionStage resolveRealmContext( String requestURL, String method, String path, Function headers) { - String realm = resolveRealmIdentifier(headers); - return () -> realm; + try { + String realm = resolveRealmIdentifier(headers); + return CompletableFuture.completedFuture(() -> realm); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } } private String resolveRealmIdentifier(Function headers) { String realm = headers.apply(configuration.headerName()); if (realm != null) { if (!configuration.realms().contains(realm)) { - throw new UnresolvableRealmContextException("Unknown realm: " + realm); + throw new IllegalArgumentException("Unknown realm: " + realm); } } else { if (configuration.requireHeader()) { - throw new UnresolvableRealmContextException( + throw new IllegalArgumentException( "Missing required realm header: " + configuration.headerName()); } realm = configuration.defaultRealm(); diff --git a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java index ae2b2dc6e4..0aea6e9bd1 100644 --- a/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java +++ b/service/common/src/main/java/org/apache/polaris/service/context/RealmContextResolver.java @@ -19,22 +19,45 @@ package org.apache.polaris.service.context; import java.util.Map; +import java.util.concurrent.CompletionStage; import java.util.function.Function; import org.apache.commons.collections.map.CaseInsensitiveMap; import org.apache.polaris.core.context.RealmContext; +/** + * An interface for resolving the realm context for a given request. + * + *

General implementation guidance: + * + *

Methods in this class should not block the calling thread. If the realm resolution process is + * blocking, it should be done in a separate thread. + * + *

In the case of an error during realm resolution, the {@link CompletionStage} should complete + * exceptionally; methods should not throw exceptions directly. + * + *

The realm resolution takes place in the very early stages of the request processing pipeline, + * and before any authentication or authorization checks are performed. Implementations should be + * careful with the use of any blocking operations, as they may lead to performance issues or + * deadlocks, especially in public environments. + */ public interface RealmContextResolver { /** - * Resolves the realm context for the given request. + * Resolves the realm context for the given request, and returns a {@link CompletionStage} that + * completes with the resolved realm context. * - * @return the resolved realm context - * @throws UnresolvableRealmContextException if the realm context cannot be resolved + * @return a {@link CompletionStage} that completes with the resolved realm context */ - RealmContext resolveRealmContext( + CompletionStage resolveRealmContext( String requestURL, String method, String path, Function headers); - default RealmContext resolveRealmContext( + /** + * Resolves the realm context for the given request, and returns a {@link CompletionStage} that + * completes with the resolved realm context. + * + * @return a {@link CompletionStage} that completes with the resolved realm context + */ + default CompletionStage resolveRealmContext( String requestURL, String method, String path, Map headers) { CaseInsensitiveMap caseInsensitiveMap = new CaseInsensitiveMap(headers); return resolveRealmContext( diff --git a/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java b/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java index 1fcff6496b..79a9149546 100644 --- a/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java +++ b/service/common/src/main/java/org/apache/polaris/service/context/TestRealmContextResolver.java @@ -24,6 +24,8 @@ import jakarta.inject.Inject; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.function.Function; import org.apache.polaris.core.context.RealmContext; import org.slf4j.Logger; @@ -50,7 +52,7 @@ public TestRealmContextResolver(RealmContextConfiguration configuration) { } @Override - public RealmContext resolveRealmContext( + public CompletionStage resolveRealmContext( String requestURL, String method, String path, Function headers) { // Since this default resolver is strictly for use in test/dev environments, we'll consider // it safe to log all contents. Any "real" resolver used in a prod environment should make @@ -72,7 +74,7 @@ public RealmContext resolveRealmContext( parsedProperties.put(REALM_PROPERTY_KEY, configuration.defaultRealm()); } String realmId = parsedProperties.get(REALM_PROPERTY_KEY); - return () -> realmId; + return CompletableFuture.completedFuture(() -> realmId); } /** diff --git a/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java b/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java deleted file mode 100644 index 4456fc602e..0000000000 --- a/service/common/src/main/java/org/apache/polaris/service/context/UnresolvableRealmContextException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 org.apache.polaris.service.context; - -import java.util.Map; -import org.apache.polaris.core.exceptions.PolarisException; - -/** - * Exception thrown when a realm context cannot be resolved. - * - * @see RealmContextResolver#resolveRealmContext(String, String, String, Map) - */ -public class UnresolvableRealmContextException extends PolarisException { - - public UnresolvableRealmContextException(String message) { - super(message); - } -} diff --git a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java index 7eac23bd55..dfbd4f6c5c 100644 --- a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java +++ b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java @@ -31,7 +31,6 @@ import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException; import org.apache.polaris.core.policy.validator.InvalidPolicyException; -import org.apache.polaris.service.context.UnresolvableRealmContextException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -48,8 +47,6 @@ public class PolarisExceptionMapper implements ExceptionMapper private Response.Status getStatus(PolarisException exception) { if (exception instanceof AlreadyExistsException) { return Response.Status.CONFLICT; - } else if (exception instanceof UnresolvableRealmContextException) { - return Response.Status.NOT_FOUND; } else if (exception instanceof InvalidPolicyException) { return Response.Status.BAD_REQUEST; } else if (exception instanceof PolicyAttachException) { diff --git a/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java b/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java index 7b114cafb7..e7d1ab70db 100644 --- a/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java +++ b/service/common/src/test/java/org/apache/polaris/service/context/DefaultRealmIdResolverTest.java @@ -45,12 +45,16 @@ void setUp() { void headerPresentSuccess() { DefaultRealmContextResolver resolver = new DefaultRealmContextResolver(config); RealmContext RealmContext1 = - resolver.resolveRealmContext( - "requestURL", "method", "path", Map.of("Polaris-Header", "realm1")); + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of("Polaris-Header", "realm1")) + .toCompletableFuture() + .join(); assertThat(RealmContext1.getRealmIdentifier()).isEqualTo("realm1"); RealmContext RealmContext2 = - resolver.resolveRealmContext( - "requestURL", "method", "path", Map.of("Polaris-Header", "realm2")); + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of("Polaris-Header", "realm2")) + .toCompletableFuture() + .join(); assertThat(RealmContext2.getRealmIdentifier()).isEqualTo("realm2"); } @@ -59,9 +63,13 @@ void headerPresentFailure() { DefaultRealmContextResolver resolver = new DefaultRealmContextResolver(config); assertThatThrownBy( () -> - resolver.resolveRealmContext( - "requestURL", "method", "path", Map.of("Polaris-Header", "realm3"))) - .isInstanceOf(UnresolvableRealmContextException.class) + resolver + .resolveRealmContext( + "requestURL", "method", "path", Map.of("Polaris-Header", "realm3")) + .toCompletableFuture() + .join()) + .rootCause() + .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unknown realm: realm3"); } @@ -70,7 +78,10 @@ void headerNotPresentSuccess() { when(config.requireHeader()).thenReturn(false); DefaultRealmContextResolver resolver = new DefaultRealmContextResolver(config); RealmContext RealmContext1 = - resolver.resolveRealmContext("requestURL", "method", "path", Map.of()); + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of()) + .toCompletableFuture() + .join(); assertThat(RealmContext1.getRealmIdentifier()).isEqualTo("realm1"); } @@ -78,8 +89,14 @@ void headerNotPresentSuccess() { void headerNotPresentFailure() { when(config.requireHeader()).thenReturn(true); DefaultRealmContextResolver resolver = new DefaultRealmContextResolver(config); - assertThatThrownBy(() -> resolver.resolveRealmContext("requestURL", "method", "path", Map.of())) - .isInstanceOf(UnresolvableRealmContextException.class) + assertThatThrownBy( + () -> + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of()) + .toCompletableFuture() + .join()) + .rootCause() + .isInstanceOf(IllegalArgumentException.class) .hasMessage("Missing required realm header: Polaris-Header"); } @@ -87,12 +104,16 @@ void headerNotPresentFailure() { void headerCaseInsensitive() { DefaultRealmContextResolver resolver = new DefaultRealmContextResolver(config); RealmContext RealmContext1 = - resolver.resolveRealmContext( - "requestURL", "method", "path", Map.of("POLARIS-HEADER", "realm1")); + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of("POLARIS-HEADER", "realm1")) + .toCompletableFuture() + .join(); assertThat(RealmContext1.getRealmIdentifier()).isEqualTo("realm1"); RealmContext RealmContext2 = - resolver.resolveRealmContext( - "requestURL", "method", "path", Map.of("polaris-header", "realm2")); + resolver + .resolveRealmContext("requestURL", "method", "path", Map.of("polaris-header", "realm2")) + .toCompletableFuture() + .join(); assertThat(RealmContext2.getRealmIdentifier()).isEqualTo("realm2"); } } From dc962745c8efce9ed647bb7cfa365d5cec0641b8 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 15 May 2025 09:25:38 -0300 Subject: [PATCH 3/5] Also set ContextLocals (just in case) --- .../polaris/service/quarkus/context/RealmContextFilter.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java index 474e768987..7ecce49b3e 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/context/RealmContextFilter.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.quarkus.context; +import io.smallrye.common.vertx.ContextLocals; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; @@ -46,6 +47,7 @@ public Uni resolveRealmContext(ContainerRequestContext rc) { rc.getHeaders()::getFirst)) .onItem() .invoke(realmContext -> rc.setProperty(REALM_CONTEXT_KEY, realmContext)) + .invoke(realmContext -> ContextLocals.put(REALM_CONTEXT_KEY, realmContext)) .onItemOrFailure() .transform((realmContext, error) -> error == null ? null : errorResponse(error)); } From 0508b4fa348b120dac6e3f371e3a4b50be514f85 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 15 May 2025 09:27:22 -0300 Subject: [PATCH 4/5] distinguish failed resolution from unresolved --- .../metrics/RealmIdTagContributor.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java index d7ffa0e469..35c32c5329 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java @@ -21,7 +21,6 @@ import io.micrometer.core.instrument.Tags; import io.quarkus.micrometer.runtime.HttpServerMetricsTagsContributor; import io.vertx.core.http.HttpServerRequest; -import jakarta.annotation.Nullable; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.polaris.core.context.RealmContext; @@ -31,6 +30,10 @@ public class RealmIdTagContributor implements HttpServerMetricsTagsContributor { public static final String TAG_REALM = "realm_id"; + public static final String TAG_REALM_RESOLUTION_FAILURE = "realm_resolution_failure"; + + private static final Tags UNFINISHED_RESOLUTION_TAGS = Tags.of(TAG_REALM, "???"); + private static final Tags FAILED_RESOLUTION_TAGS = Tags.of(TAG_REALM, "!!!"); @Inject RealmContextResolver realmContextResolver; @@ -39,26 +42,31 @@ public Tags contribute(Context context) { // FIXME request scope does not work here, so we have to resolve the realm context manually HttpServerRequest request = context.request(); try { - RealmContext realmContext = resolveRealmContext(request); - return realmContext == null - ? Tags.empty() - : Tags.of(TAG_REALM, realmContext.getRealmIdentifier()); - } catch (Exception ignored) { - // ignore, the RealmContextFilter will handle the error - return Tags.empty(); + return realmContextResolver + .resolveRealmContext( + request.absoluteURI(), + request.method().name(), + request.path(), + request.headers()::get) + .thenApply(this::successfulResolutionTags) + .exceptionally(this::failedResolutionTags) + .toCompletableFuture() + // get the result of the CompletableFuture if it's already completed, + // otherwise return UNFINISHED_RESOLUTION_TAGS as this code is executed on + // an event loop thread, and we don't want to block it. + .getNow(UNFINISHED_RESOLUTION_TAGS); + } catch (Exception e) { + return failedResolutionTags(e); } } - @Nullable - private RealmContext resolveRealmContext(HttpServerRequest request) { - return realmContextResolver - .resolveRealmContext( - request.absoluteURI(), request.method().name(), request.path(), request.headers()::get) - .exceptionally(error -> null) - .toCompletableFuture() - // get the result of the CompletableFuture if it's already completed, - // otherwise return null as this code is executed on an event loop thread, - // and we don't want to block it. - .getNow(null); + private Tags successfulResolutionTags(RealmContext realmContext) { + return Tags.of(TAG_REALM, realmContext.getRealmIdentifier()); + } + + private Tags failedResolutionTags(Throwable error) { + return FAILED_RESOLUTION_TAGS.and( + TAG_REALM_RESOLUTION_FAILURE, + error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage()); } } From 52b1ff8598d1650f38cc460460e3d7682f02f6b8 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 15 May 2025 12:31:41 -0300 Subject: [PATCH 5/5] update FIXME --- .../polaris/service/quarkus/metrics/RealmIdTagContributor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java index 35c32c5329..7dd81f95b2 100644 --- a/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java +++ b/quarkus/service/src/main/java/org/apache/polaris/service/quarkus/metrics/RealmIdTagContributor.java @@ -39,7 +39,8 @@ public class RealmIdTagContributor implements HttpServerMetricsTagsContributor { @Override public Tags contribute(Context context) { - // FIXME request scope does not work here, so we have to resolve the realm context manually + // FIXME retrieve the realm context from context.requestContextLocalData() when this PR is in: + // https://github.com/quarkusio/quarkus/pull/47887 HttpServerRequest request = context.request(); try { return realmContextResolver