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.camel.impl.health; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.List; 024import java.util.Map; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.concurrent.ConcurrentHashMap; 028import java.util.concurrent.ConcurrentMap; 029import java.util.concurrent.ScheduledExecutorService; 030import java.util.concurrent.ScheduledFuture; 031import java.util.concurrent.TimeUnit; 032import java.util.concurrent.locks.StampedLock; 033import java.util.function.BiConsumer; 034import java.util.stream.Collectors; 035 036import org.apache.camel.CamelContext; 037import org.apache.camel.health.HealthCheck; 038import org.apache.camel.health.HealthCheckHelper; 039import org.apache.camel.health.HealthCheckRegistry; 040import org.apache.camel.health.HealthCheckService; 041import org.apache.camel.support.ServiceSupport; 042import org.apache.camel.util.ObjectHelper; 043import org.apache.camel.util.concurrent.LockHelper; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047public final class DefaultHealthCheckService extends ServiceSupport implements HealthCheckService { 048 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHealthCheckService.class); 049 050 private final ConcurrentMap<HealthCheck, HealthCheck.Result> checks; 051 private final ConcurrentMap<String, Map<String, Object>> options; 052 private final List<BiConsumer<HealthCheck.State, HealthCheck>> listeners; 053 private final StampedLock lock; 054 055 private CamelContext camelContext; 056 private ScheduledExecutorService executorService; 057 private long checkInterval; 058 private TimeUnit checkIntervalUnit; 059 private volatile HealthCheckRegistry registry; 060 private volatile ScheduledFuture<?> future; 061 062 public DefaultHealthCheckService() { 063 this(null); 064 } 065 066 public DefaultHealthCheckService(CamelContext camelContext) { 067 this.checks = new ConcurrentHashMap<>(); 068 this.options = new ConcurrentHashMap<>(); 069 this.listeners = new ArrayList<>(); 070 this.lock = new StampedLock(); 071 072 this.camelContext = camelContext; 073 this.checkInterval = 30; 074 this.checkIntervalUnit = TimeUnit.SECONDS; 075 } 076 077 // ************************************ 078 // Properties 079 // ************************************ 080 081 @Override 082 public void setCamelContext(CamelContext camelContext) { 083 this.camelContext = camelContext; 084 } 085 086 @Override 087 public CamelContext getCamelContext() { 088 return camelContext; 089 } 090 091 public HealthCheckRegistry getHealthCheckRegistry() { 092 return registry; 093 } 094 095 public void setHealthCheckRegistry(HealthCheckRegistry registry) { 096 this.registry = registry; 097 } 098 099 public long getCheckInterval() { 100 return checkInterval; 101 } 102 103 public void setCheckInterval(long checkInterval) { 104 this.checkInterval = checkInterval; 105 } 106 107 public void setCheckInterval(long interval, TimeUnit intervalUnit) { 108 setCheckInterval(interval); 109 setCheckIntervalUnit(intervalUnit); 110 } 111 112 public TimeUnit getCheckIntervalUnit() { 113 return checkIntervalUnit; 114 } 115 116 public void setCheckIntervalUnit(TimeUnit checkIntervalUnit) { 117 this.checkIntervalUnit = checkIntervalUnit; 118 } 119 120 @Override 121 public void addStateChangeListener(BiConsumer<HealthCheck.State, HealthCheck> consumer) { 122 LockHelper.doWithWriteLock( 123 lock, 124 () -> listeners.add(consumer) 125 ); 126 } 127 128 @Override 129 public void removeStateChangeListener(BiConsumer<HealthCheck.State, HealthCheck> consumer) { 130 LockHelper.doWithWriteLock( 131 lock, 132 () -> listeners.removeIf(listener -> listener.equals(consumer)) 133 ); 134 } 135 136 @Override 137 public void setHealthCheckOptions(String id, Map<String, Object> options) { 138 options.put(id, options); 139 } 140 141 @Override 142 public Optional<HealthCheck.Result> call(String id) { 143 return call(id, options.getOrDefault(id, Collections.emptyMap())); 144 } 145 146 @Override 147 public Optional<HealthCheck.Result> call(String id, Map<String, Object> options) { 148 return registry.getCheck(id).map(check -> invoke(check, options)); 149 } 150 151 @Override 152 public void notify(HealthCheck check, HealthCheck.Result result) { 153 LockHelper.doWithWriteLock( 154 lock, 155 () -> processResult(check, result) 156 ); 157 } 158 159 @Override 160 public Collection<HealthCheck.Result> getResults() { 161 return new ArrayList<>(this.checks.values()); 162 } 163 164 // ************************************ 165 // Lifecycle 166 // ************************************ 167 168 @Override 169 protected void doStart() throws Exception { 170 ObjectHelper.notNull(camelContext, "CamelContext"); 171 172 if (executorService == null) { 173 executorService = camelContext.getExecutorServiceManager().newSingleThreadScheduledExecutor(this, "DefaultHealthCheckService"); 174 } 175 if (future != null) { 176 future.cancel(true); 177 } 178 if (registry == null) { 179 registry = camelContext.getHealthCheckRegistry(); 180 } 181 182 if (ObjectHelper.isNotEmpty(registry) && ObjectHelper.isEmpty(future)) { 183 // Start the health check task only if the health check registry 184 // has been registered. 185 LOGGER.debug("Schedule health-checks to be executed every %d (%s)", checkInterval, checkIntervalUnit.name()); 186 future = executorService.scheduleAtFixedRate( 187 () -> { 188 if (!isRunAllowed()) { 189 // do not invoke the check if the service is not yet 190 // properly started. 191 return; 192 } 193 194 LOGGER.debug("Invoke health-checks (scheduled)"); 195 196 registry.stream() 197 .collect(Collectors.groupingBy(HealthCheckHelper::getGroup)) 198 .entrySet().stream() 199 .map(Map.Entry::getValue) 200 .flatMap(Collection::stream) 201 .sorted(Comparator.comparingInt(HealthCheck::getOrder)) 202 .forEach(this::invoke); 203 }, 204 checkInterval, 205 checkInterval, 206 checkIntervalUnit); 207 } 208 } 209 210 @Override 211 protected void doStop() throws Exception { 212 if (future != null) { 213 future.cancel(true); 214 future = null; 215 } 216 if (executorService != null) { 217 if (camelContext != null) { 218 camelContext.getExecutorServiceManager().shutdownNow(executorService); 219 } else { 220 executorService.shutdownNow(); 221 } 222 executorService = null; 223 } 224 } 225 226 // ************************************ 227 // Helpers 228 // ************************************ 229 230 private HealthCheck.Result processResult(HealthCheck check, HealthCheck.Result result) { 231 final HealthCheck.Result cachedResult = checks.get(check); 232 233 if (!isSameResult(result, cachedResult)) { 234 // Maybe make the listener aware of the reason, i.e. 235 // the service is still un-healthy but the message 236 // or error has changed. 237 listeners.forEach(listener -> listener.accept(result.getState(), check)); 238 } 239 240 // replace the old result with the new one even if the 241 // state has not changed but the reason/error may be 242 // changed. 243 checks.put(check, result); 244 245 return result; 246 } 247 248 private HealthCheck.Result invoke(HealthCheck check) { 249 return invoke(check, options.getOrDefault(check.getId(), Collections.emptyMap())); 250 } 251 252 private HealthCheck.Result invoke(HealthCheck check, Map<String, Object> options) { 253 return LockHelper.supplyWithWriteLock( 254 lock, 255 () -> { 256 LOGGER.debug("Invoke health-check {}", check.getId()); 257 return processResult(check, check.call(options)); 258 } 259 ); 260 } 261 262 /** 263 * Check if two results are equals by checking only the state, this method 264 * does not check if the result comes from the same health check, this should 265 * be done by the caller. 266 * <p> 267 * A future implementation should check all the parameter of the result. 268 */ 269 private boolean isSameResult(HealthCheck.Result r1, HealthCheck.Result r2) { 270 if (Objects.equals(r1, r2)) { 271 return true; 272 } 273 274 if (r1 != null && r2 != null) { 275 return r1.getState() == r2.getState(); 276 } 277 278 return false; 279 } 280}