palantir / palantir/conjure-java

Extension point for endpoint specific handler behaviour in conjure-undertow

Open
#527 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
39
Forks
49
Avg merge
8h 22m
Merged PRs (30d)
32

Description

What happened?

I had forked generated undertow handlers to add metrics dependent on request body.

Currently you can modify your service by wrapping your service definition or wrapping the endpoint. However, there's no way to create a plugin that can use both of them without forking generated undertow handlers.

The concrete case was creating a server response time metric that has tag values that are request body specific. This is a simpler version of the code modifications I had to make to generated service endpoints. Naturally this gets pretty cumbersome with many endpoints.

public final class SomeServiceEndpoints implements UndertowService {
    private final DatasetSizeThresholds sizeThresholds;
    private final MetricsSink metrics;
    private final SomeService delegate;

    private SomeServiceEndpoints(
            DatasetSizeThresholds sizeThresholds, MetricsSink metrics, SomeService delegate) {
        this.sizeThresholds = sizeThresholds;
        this.metrics = metrics;
        this.delegate = delegate;
    }

    public static UndertowService of(
            DatasetSizeThresholds sizeThresholds,
            MetricsSink metrics,
            SomeService delegate) {
        return new SomeServiceEndpoints(sizeThresholds, metrics, delegate);
    }

    @Override
    public List<Endpoint> endpoints(UndertowRuntime runtime) {
        return Collections.unmodifiableList(
                Arrays.asList(
                        new GetPreviewEndpoint(runtime, delegate, sizeThresholds, metrics)));
    }

    private static final class GetPreviewEndpoint
            implements HttpHandler, Endpoint, ReturnValueWriter<BinaryResponseBody> {
        private final UndertowRuntime runtime;
        private final SomeService delegate;
        private final Deserializer<PreviewDataRequest> deserializer;
        private final DatasetSizeThresholds sizeThresholds;
        private final MetricsSink metrics;

        GetPreviewEndpoint(
                UndertowRuntime runtime,
                SomeService delegate,
                DatasetSizeThresholds sizeThresholds,
                MetricsSink metrics) {
            this.runtime = runtime;
            this.delegate = delegate;
            this.deserializer =
                    runtime.bodySerDe().deserializer(new TypeMarker<PreviewDataRequest>() {});
            this.sizeThresholds = sizeThresholds;
            this.metrics = metrics;
        }

        @Override
        public void handleRequest(HttpServerExchange exchange) throws IOException {
            AuthHeader authHeader = runtime.auth().header(exchange);
            PreviewDataRequest previewRequest = deserializer.deserialize(exchange);
            exchange.addExchangeCompleteListener(buildExchangeCompletionListener(
                    metrics,
                    () -> sizeThresholds.getDatasetSize(
                            authHeader,
                            previewRequest)
                            .sizeInBytes()));
            ListenableFuture<BinaryResponseBody> result =
                    delegate.getPreview(authHeader, previewRequest);
            runtime.async().register(result, this, exchange);
        }

        @Override
        public void write(BinaryResponseBody result, HttpServerExchange exchange)
                throws IOException {
            runtime.bodySerDe().serialize(result, exchange);
        }

        @Override
        public HttpString method() {
            return Methods.POST;
        }

        @Override
        public String template() {
            return "/someservice/arrow";
        }

        @Override
        public String serviceName() {
            return "SomeService";
        }

        @Override
        public String name() {
            return "getPreview";
        }

        @Override
        public HttpHandler handler() {
            return this;
        }
    }

    private static void recordMetrics(
            MetricsSink metrics, HttpServerExchange completeExchange, long inputDatasetSize) {
        metrics.status(completeExchange.getStatusCode(), inputDatasetSize);
        metrics.durationNanoseconds(System.nanoTime() - completeExchange.getRequestStartTime(), inputDatasetSize);
    }

    private static ExchangeCompletionListener buildExchangeCompletionListener(
            MetricsSink metrics, Supplier<Optional<Long>> sizeProvider) {
        return (exchange, nextListener) -> {
            try {
                sizeProvider.get().ifPresent(datasetSize -> recordMetrics(metrics, exchange, datasetSize));
            } finally {
                nextListener.proceed();
            }
        };
    }
}

What did you want to happen?

It should be possible to write a an extension to generated handlers that can access both request objects and the undertow exchange.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the generated Undertow handlers and the extension points described in the issue's example. Define and implement an extension mechanism that can access both deserialized request objects and the Undertow exchange, then verify that endpoint-specific behavior no longer requires forking generated handlers.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.