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}