001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.health;
018
019import static java.util.concurrent.TimeUnit.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.time.*;
024import java.util.*;
025import java.util.concurrent.*;
026
027import org.apache.juneau.rest.server.*;
028
029/**
030 * Shared health-probe aggregation worker bean.
031 *
032 * <p>
033 * Holds the capability logic for evaluating {@link HealthIndicator} beans and summarizing them into a
034 * probe response. The {@link HealthServlet} (servlet flavor) and {@link HealthMixin} (mixin flavor) are
035 * independent {@code @Rest} classes that each delegate to a {@code HealthAggregator}, so the two
036 * deployment forms cannot drift.
037 *
038 * @since 10.0.0
039 */
040@SuppressWarnings({
041   "resource" // BeanStore is not owned here; its lifecycle is managed by RestContext.
042})
043public class HealthAggregator {
044
045   /**
046    * Resolves the {@link HealthIndicator} beans registered in the given context's bean store.
047    *
048    * @param context The REST context whose bean store supplies the indicators.
049    * @return The indicator beans keyed by bean name.
050    */
051   public Map<String,HealthIndicator> indicators(RestContext context) {
052      return context.getBeanStore().getBeansOfType(HealthIndicator.class);
053   }
054
055   /**
056    * Evaluates the supplied indicators, summarizes their statuses, and sets the response status.
057    *
058    * @param context The REST context (supplies the {@link HealthProbeSettings} timeout).
059    * @param indicators The indicators to evaluate.
060    * @param probe The probe filter, or {@code null} to evaluate all indicators.
061    * @param res The response whose status code is set (200 healthy, 503 down).
062    * @return The aggregated health payload.
063    */
064   public HealthResponse aggregate(RestContext context, Map<String,HealthIndicator> indicators, HealthProbe probe, RestResponse res) {
065      var out = new LinkedHashMap<String,ComponentHealth>();
066      var timeout = context.getBeanStore().getBean(HealthProbeSettings.class).map(HealthProbeSettings::getTimeout).orElse(Duration.ofSeconds(1));
067      var timeoutMillis = Math.max(1L, timeout.toMillis());
068
069      for (var e : indicators.entrySet()) {
070         var indicator = e.getValue();
071         if (probe != null && !indicator.probes().contains(probe))
072            continue;
073         var componentName = firstNonEmpty(e.getKey(), cns(indicator));
074         out.put(componentName, runIndicator(componentName, indicator, timeoutMillis));
075      }
076
077      var status = summarize(out.values());
078
079      // Readiness gate: when shutdown has flipped the readiness state out of service, force the READY probe
080      // down so /readyz returns 503 before the connector closes.  /livez (LIVE) and /healthz (null) are unaffected.
081      if (probe == HealthProbe.READY && ! ReadinessState.resolve(context.getBeanStore()).isReady()) {
082         status = HealthStatus.DOWN;
083         out.put("readiness", new ComponentHealth(HealthStatus.DOWN, Map.of("state", "OUT_OF_SERVICE")));
084      }
085
086      res.setStatus(status == HealthStatus.DOWN ? 503 : 200);
087      return new HealthResponse(status, out);
088   }
089
090   private static ComponentHealth runIndicator(String name, HealthIndicator indicator, long timeoutMillis) {
091      var future = CompletableFuture.supplyAsync(indicator::check);
092      try {
093         var h = future.get(timeoutMillis, MILLISECONDS);
094         return ComponentHealth.from(h);
095      } catch (TimeoutException e) {
096         future.cancel(true);
097         return ComponentHealth.from(Health.down(name, e).detail("error", "Health check timed out after " + timeoutMillis + "ms").build());
098      } catch (ExecutionException e) {
099         return ComponentHealth.from(Health.down(name, e.getCause()).build());
100      } catch (InterruptedException e) {
101         Thread.currentThread().interrupt();
102         return ComponentHealth.from(Health.down(name, e).build());
103      } catch (Exception e) {
104         return ComponentHealth.from(Health.down(name, e).build());
105      }
106   }
107
108   private static HealthStatus summarize(Collection<ComponentHealth> components) {
109      var hasUnknown = false;
110      for (var c : components) {
111         if (c.status == HealthStatus.DOWN)
112            return HealthStatus.DOWN;
113         if (c.status == HealthStatus.UNKNOWN)
114            hasUnknown = true;
115      }
116      return hasUnknown ? HealthStatus.UNKNOWN : HealthStatus.UP;
117   }
118
119   /**
120    * Probe response payload.
121    */
122   public static class HealthResponse {
123      private final HealthStatus status;
124      private final Map<String,ComponentHealth> components;
125
126      HealthResponse(HealthStatus status, Map<String,ComponentHealth> components) {
127         this.status = status;
128         this.components = components;
129      }
130
131      /**
132       * @return The aggregated probe status.
133       */
134      public HealthStatus getStatus() { return status; }
135
136      /**
137       * @return The per-component health entries.
138       */
139      public Map<String,ComponentHealth> getComponents() { return components; }
140   }
141
142   /**
143    * Component payload in the response.
144    */
145   public static class ComponentHealth {
146      private final HealthStatus status;
147      private final Map<String,Object> details;
148
149      ComponentHealth(HealthStatus status, Map<String,Object> details) {
150         this.status = status;
151         this.details = details;
152      }
153
154      static ComponentHealth from(Health h) {
155         var details = new LinkedHashMap<>(h.getDetails());
156         if (h.getError() != null)
157            details.putIfAbsent("error", h.getError().toString());
158         return new ComponentHealth(h.getStatus(), details);
159      }
160
161      /**
162       * @return The component status.
163       */
164      public HealthStatus getStatus() { return status; }
165
166      /**
167       * @return The component detail map.
168       */
169      public Map<String,Object> getDetails() { return details; }
170   }
171}