diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/ExemplarServiceManager.java b/metrics/metrics/src/main/java/io/helidon/metrics/ExemplarServiceManager.java index 4b83f51f60b..eae45182940 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/ExemplarServiceManager.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/ExemplarServiceManager.java @@ -35,10 +35,11 @@ class ExemplarServiceManager { private static final List EXEMPLAR_SERVICES = collectExemplarServices(); + private static final boolean IS_ACTIVE = !EXEMPLAR_SERVICES.isEmpty(); - private static final Supplier EXEMPLAR_SUPPLIER = EXEMPLAR_SERVICES.isEmpty() - ? () -> "" - : () -> EXEMPLAR_SERVICES.stream() + static final String INACTIVE_LABEL = ""; + + private static final Supplier EXEMPLAR_SUPPLIER = () -> EXEMPLAR_SERVICES.stream() .map(ExemplarService::label) .filter(Predicate.not(String::isBlank)) .collect(ExemplarServiceManager::labelsStringJoiner, StringJoiner::add, StringJoiner::merge) @@ -58,7 +59,15 @@ private static StringJoiner labelsStringJoiner() { * @return exemplar string provided by the highest-priority service instance */ static String exemplarLabel() { - return EXEMPLAR_SUPPLIER.get(); + return IS_ACTIVE ? EXEMPLAR_SUPPLIER.get() : INACTIVE_LABEL; + } + + /** + * + * @return whether exemplar handling is active or not + */ + static boolean isActive() { + return IS_ACTIVE; } private static List collectExemplarServices() { diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/ExponentiallyDecayingReservoir.java b/metrics/metrics/src/main/java/io/helidon/metrics/ExponentiallyDecayingReservoir.java index be727672aa5..fba9f171aaa 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/ExponentiallyDecayingReservoir.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/ExponentiallyDecayingReservoir.java @@ -17,11 +17,16 @@ package io.helidon.metrics; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; import static java.lang.Math.exp; import static java.lang.Math.min; @@ -49,6 +54,49 @@ class ExponentiallyDecayingReservoir { private static final double DEFAULT_ALPHA = 0.015; private static final long RESCALE_THRESHOLD = TimeUnit.HOURS.toNanos(1); + /* + * Avoid computing the current time in seconds during every reservoir update by updating its value on a scheduled basis. + */ + private static final long CURRENT_TIME_IN_SECONDS_UPDATE_INTERVAL_MS = 250; + + private static final List CURRENT_TIME_IN_SECONDS_UPDATERS = new ArrayList<>(); + + private static final ScheduledExecutorService CURRENT_TIME_UPDATER_EXECUTOR_SERVICE = initCurrentTimeUpdater(); + + private static final Logger LOGGER = Logger.getLogger(ExponentiallyDecayingReservoir.class.getName()); + + private volatile long currentTimeInSeconds; + + private static ScheduledExecutorService initCurrentTimeUpdater() { + ScheduledExecutorService result = Executors.newSingleThreadScheduledExecutor(); + result.scheduleAtFixedRate(ExponentiallyDecayingReservoir::updateCurrentTimeInSecondsForAllReservoirs, + CURRENT_TIME_IN_SECONDS_UPDATE_INTERVAL_MS, + CURRENT_TIME_IN_SECONDS_UPDATE_INTERVAL_MS, TimeUnit.MILLISECONDS); + return result; + } + + static void onServerShutdown() { + CURRENT_TIME_UPDATER_EXECUTOR_SERVICE.shutdown(); + try { + boolean stoppedNormally = + CURRENT_TIME_UPDATER_EXECUTOR_SERVICE.awaitTermination(CURRENT_TIME_IN_SECONDS_UPDATE_INTERVAL_MS * 10, + TimeUnit.MILLISECONDS); + if (!stoppedNormally) { + LOGGER.log(Level.WARNING, "Shutdown of current time updater timed out; continuing"); + } + } catch (InterruptedException e) { + LOGGER.log(Level.WARNING, "InterruptedException caught while stopping the current time updater; continuing"); + } + } + + private static void updateCurrentTimeInSecondsForAllReservoirs() { + CURRENT_TIME_IN_SECONDS_UPDATERS.forEach(Runnable::run); + } + + private long computeCurrentTimeInSeconds() { + return TimeUnit.MILLISECONDS.toSeconds(clock.milliTime()); + } + private final ConcurrentSkipListMap values; private final ReentrantReadWriteLock lock; private final double alpha; @@ -81,7 +129,9 @@ class ExponentiallyDecayingReservoir { this.alpha = alpha; this.size = size; this.count = new AtomicLong(0); - this.startTime = currentTimeInSeconds(); + CURRENT_TIME_IN_SECONDS_UPDATERS.add(this::computeCurrentTimeInSeconds); + currentTimeInSeconds = computeCurrentTimeInSeconds(); + this.startTime = currentTimeInSeconds; this.nextScaleTime = new AtomicLong(clock.nanoTick() + RESCALE_THRESHOLD); } @@ -90,7 +140,7 @@ public int size() { } public void update(long value, String label) { - update(value, currentTimeInSeconds(), label); + update(value, currentTimeInSeconds, label); } /** @@ -143,10 +193,6 @@ public WeightedSnapshot getSnapshot() { } } - private long currentTimeInSeconds() { - return TimeUnit.MILLISECONDS.toSeconds(clock.milliTime()); - } - private double weight(long t) { return exp(alpha * t); } @@ -174,7 +220,7 @@ private void rescale(long now, long next) { try { if (nextScaleTime.compareAndSet(next, now + RESCALE_THRESHOLD)) { final long oldStartTime = startTime; - this.startTime = currentTimeInSeconds(); + this.startTime = currentTimeInSeconds; final double scalingFactor = exp(-alpha * (startTime - oldStartTime)); if (Double.compare(scalingFactor, 0) == 0) { values.clear(); diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/HelidonMetric.java b/metrics/metrics/src/main/java/io/helidon/metrics/HelidonMetric.java index 67a9d2211fb..f527b9c8309 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/HelidonMetric.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/HelidonMetric.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,4 +74,17 @@ interface HelidonMetric extends Metric { * @return the metric's {@link Metadata} */ Metadata metadata(); + + /** + * Returns whether the metric has been deleted. + * + * @return true if the metrics was removed from the registry; false otherwise + */ + boolean isDeleted(); + + /** + * Mark this metric as deleted. + */ + void markAsDeleted(); + } diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/MetricImpl.java b/metrics/metrics/src/main/java/io/helidon/metrics/MetricImpl.java index f53bd7220ce..868d4bd993a 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/MetricImpl.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/MetricImpl.java @@ -125,6 +125,9 @@ private static Map initEscapedCharsMap() { private final String registryType; private final Metadata metadata; + // Efficient check from interceptors to see if the metric is still valid + private boolean isDeleted; + MetricImpl(String registryType, Metadata metadata) { this.metadata = metadata; this.registryType = registryType; @@ -188,6 +191,16 @@ public void jsonMeta(JsonObjectBuilder builder, List metricIDs) { builder.add(getName(), metaBuilder); } + @Override + public boolean isDeleted() { + return isDeleted; + } + + @Override + public void markAsDeleted() { + isDeleted = true; + } + static String jsonFullKey(String baseName, MetricID metricID) { return metricID.getTags().isEmpty() ? baseName : String.format("%s;%s", baseName, diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java b/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java index 6f09198a918..d3e7ee7cb76 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/MetricsSupport.java @@ -430,6 +430,11 @@ public void update(Routing.Rules rules) { configureEndpoint(rules, rules); } + @Override + protected void onShutdown() { + ExponentiallyDecayingReservoir.onServerShutdown(); + } + private static KeyPerformanceIndicatorSupport.Context kpiContext(ServerRequest request) { return request.context() .get(KeyPerformanceIndicatorSupport.Context.class) diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/Registry.java b/metrics/metrics/src/main/java/io/helidon/metrics/Registry.java index e52fc5d5c42..b1e193e2d61 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/Registry.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/Registry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. + * Copyright (c) 2018, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,17 @@ public static Registry create(Type type) { return new Registry(type); } + /** + * Indicates whether the specific metrics has been marked as deleted. + * + * @param metric the metric to check + * @return true if it's a Helidon metric and has been marked as deleted; false otherwise + */ + public static boolean isMarkedAsDeleted(Metric metric) { + return (metric instanceof HelidonMetric) + && ((HelidonMetric) metric).isDeleted(); + } + @Override public T register(String name, T metric) throws IllegalArgumentException { return registerUniqueMetric(name, metric); @@ -225,12 +236,14 @@ public SimpleTimer simpleTimer(Metadata metadata, Tag... tags) { */ @Override public synchronized boolean remove(String name) { - final List metricIDs = allMetricIDsByName.get(name); - if (metricIDs == null) { + final List> doomedMetrics = getMetricsByName(name); + if (doomedMetrics.isEmpty()) { return false; } - final boolean result = metricIDs.stream() - .map(metricID -> allMetrics.remove(metricID) != null) + + final boolean result = doomedMetrics.stream() + .peek(entry -> entry.getValue().markAsDeleted()) + .map(entry -> allMetrics.remove(entry.getKey()) != null) .reduce((a, b) -> a || b) .orElse(false); allMetricIDsByName.remove(name); @@ -256,7 +269,11 @@ public synchronized boolean remove(MetricID metricID) { allMetadata.remove(metricID.getName()); } - return allMetrics.remove(metricID) != null; + HelidonMetric doomedMetric = allMetrics.remove(metricID); + if (doomedMetric != null) { + doomedMetric.markAsDeleted(); + } + return doomedMetric != null; } @Override diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/Sample.java b/metrics/metrics/src/main/java/io/helidon/metrics/Sample.java index 298197c6779..ec17f82dc93 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/Sample.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/Sample.java @@ -21,6 +21,8 @@ */ interface Sample { + boolean IS_EXEMPLAR_HANDLING_ACTIVE = ExemplarServiceManager.isActive(); + static Derived derived(double value, Sample.Labeled reference) { return new Derived.Impl(value, reference); } @@ -30,7 +32,9 @@ static Derived derived(double value) { } static Labeled labeled(long value) { - return new Labeled.Impl(value, ExemplarServiceManager.exemplarLabel(), System.currentTimeMillis()); + return IS_EXEMPLAR_HANDLING_ACTIVE + ? new Labeled.Impl(value, ExemplarServiceManager.exemplarLabel(), System.currentTimeMillis()) + : new Labeled.Impl(value, ExemplarServiceManager.INACTIVE_LABEL, 0); } /** diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/WeightedSnapshot.java b/metrics/metrics/src/main/java/io/helidon/metrics/WeightedSnapshot.java index a179bd7abbd..dac16230852 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/WeightedSnapshot.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/WeightedSnapshot.java @@ -321,6 +321,9 @@ public void dump(OutputStream output) { /** * Labeled sample with a weight. + * + * If the label is empty, then this sample will never be an exemplar so we do not need to default the timestamp to the + * current time. */ static class WeightedSample extends Labeled.Impl { @@ -335,11 +338,11 @@ static class WeightedSample extends Labeled.Impl { } WeightedSample(long value, double weight, String label) { - this(value, weight, System.currentTimeMillis(), label); + this(value, weight, label.isEmpty() ? 0 : System.currentTimeMillis(), label); } WeightedSample(long value) { - this(value, 1.0, System.currentTimeMillis(), ""); + this(value, 1.0, 0, ""); } long getValue() { diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorBase.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorBase.java index 06bc1126949..3cc27d2273b 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorBase.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorBase.java @@ -17,7 +17,6 @@ package io.helidon.microprofile.metrics; import java.lang.annotation.Annotation; -import java.util.Map; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,11 +24,11 @@ import javax.inject.Inject; import javax.interceptor.InvocationContext; +import io.helidon.metrics.Registry; import io.helidon.microprofile.metrics.MetricsCdiExtension.MetricWorkItem; import io.helidon.servicecommon.restcdi.HelidonInterceptor; import org.eclipse.microprofile.metrics.Metric; -import org.eclipse.microprofile.metrics.MetricID; import org.eclipse.microprofile.metrics.MetricRegistry; /** @@ -53,8 +52,6 @@ abstract class InterceptorBase extends HelidonInterceptor.Base @Inject private MetricRegistry registry; - private Map metricsForVerification; - enum ActionType { PREINVOKE("preinvoke"), COMPLETE("complete"); @@ -78,27 +75,22 @@ Class annotationType() { return annotationType; } - Map metricsForVerification() { - if (metricsForVerification == null) { - metricsForVerification = registry.getMetrics(); - } - return metricsForVerification; - } - @Override public void preInvocation(InvocationContext context, MetricWorkItem workItem) { invokeVerifiedAction(context, workItem, this::preInvoke, ActionType.PREINVOKE); } void invokeVerifiedAction(InvocationContext context, MetricWorkItem workItem, Consumer action, ActionType actionType) { - if (!metricsForVerification().containsKey(workItem.metricID())) { + Metric metric = workItem.metric(); + if (Registry.isMarkedAsDeleted(metric)) { throw new IllegalStateException("Attempt to use previously-removed metric" + workItem.metricID()); } - Metric metric = workItem.metric(); - LOGGER.log(Level.FINEST, () -> String.format( - "%s (%s) is accepting %s %s for processing on %s triggered by @%s", - getClass().getSimpleName(), actionType, workItem.metric().getClass().getSimpleName(), workItem.metricID(), - context.getMethod() != null ? context.getMethod() : context.getConstructor(), annotationType.getSimpleName())); + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, String.format( + "%s (%s) is accepting %s %s for processing on %s triggered by @%s", + getClass().getSimpleName(), actionType, workItem.metric().getClass().getSimpleName(), workItem.metricID(), + context.getMethod() != null ? context.getMethod() : context.getConstructor(), annotationType.getSimpleName())); + } action.accept(metricType.cast(metric)); } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSyntheticSimplyTimed.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSyntheticSimplyTimed.java index f70bac3553a..4846e4c490b 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSyntheticSimplyTimed.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/InterceptorSyntheticSimplyTimed.java @@ -16,17 +16,10 @@ package io.helidon.microprofile.metrics; -import java.util.Map; - import javax.annotation.Priority; import javax.inject.Inject; import javax.interceptor.Interceptor; -import org.eclipse.microprofile.metrics.Metric; -import org.eclipse.microprofile.metrics.MetricID; -import org.eclipse.microprofile.metrics.MetricRegistry; -import org.eclipse.microprofile.metrics.annotation.RegistryType; - /** * Interceptor for synthetic {@link SyntheticSimplyTimed} annotations. *

@@ -39,16 +32,8 @@ @Priority(Interceptor.Priority.PLATFORM_BEFORE + 10) final class InterceptorSyntheticSimplyTimed extends InterceptorSimplyTimedBase { - private final Map metricsForVerification; - @Inject - InterceptorSyntheticSimplyTimed(@RegistryType(type = MetricRegistry.Type.BASE) MetricRegistry baseRegistry) { + InterceptorSyntheticSimplyTimed() { super(SyntheticSimplyTimed.class); - metricsForVerification = baseRegistry.getMetrics(); - } - - @Override - Map metricsForVerification() { - return metricsForVerification; } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsInterceptorBase.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsInterceptorBase.java index e25e174181d..ecb4661e172 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsInterceptorBase.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsInterceptorBase.java @@ -18,19 +18,17 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Executable; -import java.util.Map; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Inject; import javax.interceptor.InvocationContext; +import io.helidon.metrics.Registry; import io.helidon.microprofile.metrics.MetricsCdiExtension.MetricWorkItem; import io.helidon.servicecommon.restcdi.HelidonInterceptor; import org.eclipse.microprofile.metrics.Metric; -import org.eclipse.microprofile.metrics.MetricID; import org.eclipse.microprofile.metrics.MetricRegistry; /** @@ -54,8 +52,6 @@ abstract class MetricsInterceptorBase extends HelidonIntercept @Inject private MetricRegistry registry; - private Map metricsForVerification; - enum ActionType { PREINVOKE("preinvoke"), COMPLETE("complete"); @@ -75,13 +71,6 @@ public String toString() { this.metricType = metricType; } - Map metricsForVerification() { - if (metricsForVerification == null) { - metricsForVerification = registry.getMetrics(); - } - return metricsForVerification; - } - @Override public Iterable workItems(Executable executable) { return extension.workItems(executable, annotationType); @@ -89,19 +78,23 @@ public Iterable workItems(Executable executable) { @Override public void preInvocation(InvocationContext context, MetricWorkItem workItem) { - invokeVerifiedAction(context, workItem, this::preInvoke, ActionType.PREINVOKE); + verifyMetric(context, workItem, ActionType.PREINVOKE); + preInvoke(metricType.cast(workItem.metric())); } - void invokeVerifiedAction(InvocationContext context, MetricWorkItem workItem, Consumer action, ActionType actionType) { - if (!metricsForVerification().containsKey(workItem.metricID())) { + void verifyMetric(InvocationContext context, MetricWorkItem workItem, ActionType actionType) { + Metric metric = workItem.metric(); + if (Registry.isMarkedAsDeleted(metric)) { throw new IllegalStateException("Attempt to use previously-removed metric" + workItem.metricID()); } - Metric metric = workItem.metric(); - LOGGER.log(Level.FINEST, () -> String.format( - "%s (%s) is accepting %s %s for processing on %s triggered by @%s", - getClass().getSimpleName(), actionType, workItem.metric().getClass().getSimpleName(), workItem.metricID(), - context.getMethod() != null ? context.getMethod() : context.getConstructor(), annotationType.getSimpleName())); - action.accept(metricType.cast(metric)); + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, String.format( + "%s (%s) is accepting %s %s for processing on %s triggered by @%s", + getClass().getSimpleName(), actionType, workItem.metric() + .getClass() + .getSimpleName(), workItem.metricID(), + context.getMethod() != null ? context.getMethod() : context.getConstructor(), annotationType.getSimpleName())); + } } abstract void preInvoke(M metric); @@ -118,13 +111,17 @@ void invokeVerifiedAction(InvocationContext context, MetricWorkItem workItem, Co abstract static class WithPostCompletion extends MetricsInterceptorBase implements HelidonInterceptor.WithPostCompletion { + private final Class metricType; + WithPostCompletion(Class annotationType, Class metricType) { super(annotationType, metricType); + this.metricType = metricType; } @Override public void postCompletion(InvocationContext context, Throwable throwable, MetricWorkItem workItem) { - invokeVerifiedAction(context, workItem, this::postComplete, ActionType.COMPLETE); + verifyMetric(context, workItem, ActionType.COMPLETE); + postComplete(metricType.cast(workItem.metric())); } abstract void postComplete(T metric); diff --git a/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/HelidonRestCdiExtension.java b/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/HelidonRestCdiExtension.java index a01c2aa1a77..938c1fe57ff 100644 --- a/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/HelidonRestCdiExtension.java +++ b/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/HelidonRestCdiExtension.java @@ -19,9 +19,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Executable; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -265,19 +267,23 @@ public static WorkItemsManager create() { private WorkItemsManager() { } - private final Map, Set>> workItemsByExecutable = new HashMap<>(); + private final Map, List>> workItemsByExecutable = new HashMap<>(); public void put(Executable executable, Class annotationType, W workItem) { - workItemsByExecutable + List workItems = workItemsByExecutable .computeIfAbsent(executable, e -> new HashMap<>()) - .computeIfAbsent(annotationType, t -> new HashSet<>()) - .add(workItem); + .computeIfAbsent(annotationType, t -> new ArrayList<>()); + // This method is invoked only during annotation processing from CDI extensions, so linear scans of the lists + // does not hurt runtime performance during request handling. + if (!workItems.contains(workItem)) { + workItems.add(workItem); + } } public Iterable workItems(Executable executable, Class annotationType) { return workItemsByExecutable .getOrDefault(executable, Collections.emptyMap()) - .getOrDefault(annotationType, Collections.emptySet()); + .getOrDefault(annotationType, Collections.emptyList()); } } diff --git a/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/InterceptionRunnerImpl.java b/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/InterceptionRunnerImpl.java index 13630f203d1..04363ac2073 100644 --- a/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/InterceptionRunnerImpl.java +++ b/service-common/rest-cdi/src/main/java/io/helidon/servicecommon/restcdi/InterceptionRunnerImpl.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Objects; import java.util.StringJoiner; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Level; @@ -52,6 +53,8 @@ class InterceptionRunnerImpl implements InterceptionRunner { */ private static final InterceptionRunner INSTANCE = new InterceptionRunnerImpl(); + private static final Map ASYNC_RESPONSE_SLOTS = new ConcurrentHashMap<>(); + /** * Returns the appropriate {@code InterceptionRunner} for the executable. * @@ -76,7 +79,9 @@ public Object run( InvocationContext context, Iterable workItems, PreInvocationHandler preInvocationHandler) throws Exception { - workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem)); + for (T workItem : workItems) { + preInvocationHandler.accept(context, workItem); + } return context.proceed(); } @@ -86,7 +91,9 @@ public Object run( Iterable workItems, PreInvocationHandler preInvocationHandler, PostCompletionHandler postCompletionHandler) throws Exception { - workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem)); + for (T workItem : workItems) { + preInvocationHandler.accept(context, workItem); + } Object result = null; Exception exceptionFromContextProceed = null; @@ -135,7 +142,9 @@ public Object run( // use it in the completion callback. Any other null argument would trigger an NPE from the current call stack. Objects.requireNonNull(postCompletionHandler, "postCompletionHandler"); - workItems.forEach(workItem -> preInvocationHandler.accept(context, workItem)); + for (T workItem : workItems) { + preInvocationHandler.accept(context, workItem); + } Object[] params = context.getParameters(); AsyncResponse asyncResponse = AsyncResponse.class.cast(context.getParameters()[asyncResponseSlot]); @@ -315,13 +324,16 @@ private static X processPostInvocationHandlers(Invocati } private static int asyncResponseSlot(Method interceptedMethod) { - int result = 0; + return ASYNC_RESPONSE_SLOTS.computeIfAbsent(interceptedMethod, InterceptionRunnerImpl::computeAsyncResponseSlot); + } + private static int computeAsyncResponseSlot(Method interceptedMethod) { + int newResult = 0; for (Parameter p : interceptedMethod.getParameters()) { if (AsyncResponse.class.isAssignableFrom(p.getType()) && p.getAnnotation(Suspended.class) != null) { - return result; + return newResult; } - result++; + newResult++; } return -1; } diff --git a/service-common/rest/src/main/java/io/helidon/servicecommon/rest/HelidonRestServiceSupport.java b/service-common/rest/src/main/java/io/helidon/servicecommon/rest/HelidonRestServiceSupport.java index 73184219c2b..a7e24548742 100644 --- a/service-common/rest/src/main/java/io/helidon/servicecommon/rest/HelidonRestServiceSupport.java +++ b/service-common/rest/src/main/java/io/helidon/servicecommon/rest/HelidonRestServiceSupport.java @@ -46,6 +46,7 @@ public abstract class HelidonRestServiceSupport implements Service { private final String context; private final CorsEnabledServiceHelper corsEnabledServiceHelper; private final Logger logger; + private int webServerCount; /** * Shared initialization for new service support instances. @@ -83,6 +84,11 @@ public final void configureEndpoint(Routing.Rules rules) { * @param serviceEndpointRoutingRules actual rules (if different from default) for the service endpoint */ public final void configureEndpoint(Routing.Rules defaultRules, Routing.Rules serviceEndpointRoutingRules) { + defaultRules.onNewWebServer(webserver -> { + webServerStarted(); + webserver.whenShutdown() + .thenRun(this::webServerStopped); + }); // CORS first defaultRules.any(context, corsEnabledServiceHelper.processor()); if (defaultRules != serviceEndpointRoutingRules) { @@ -99,6 +105,19 @@ public final void configureEndpoint(Routing.Rules defaultRules, Routing.Rules se */ protected abstract void postConfigureEndpoint(Routing.Rules defaultRules, Routing.Rules serviceEndpointRoutingRules); + private void webServerStarted() { + webServerCount++; + } + + private void webServerStopped() { + if (--webServerCount == 0) { + onShutdown(); + } + } + + protected void onShutdown() { + } + protected String context() { return context; }