/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.linecorp.armeria.client.endpoint.healthcheck; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.linecorp.armeria.common.util.Functions.voidFunction; import static java.util.Objects.requireNonNull; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import com.codahale.metrics.MetricSet; import com.google.common.collect.ImmutableList; import com.spotify.futures.CompletableFutures; import com.linecorp.armeria.client.ClientFactory; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.client.endpoint.DynamicEndpointGroup; import com.linecorp.armeria.client.endpoint.EndpointGroup; /** * An {@link EndpointGroup} decorator that only provides healthy {@link Endpoint}s. */ public abstract class HealthCheckedEndpointGroup extends DynamicEndpointGroup { protected static final Duration DEFAULT_HEALTHCHECK_RETRY_INTERVAL = Duration.ofSeconds(3); private final ClientFactory clientFactory; private final EndpointGroup delegate; private final Duration healthCheckRetryInterval; volatile List<ServerConnection> allServers = ImmutableList.of(); /** * Creates a new instance. * A subclass being initialized with this constructor must call {@link #init()} before start being used. */ protected HealthCheckedEndpointGroup(ClientFactory clientFactory, EndpointGroup delegate, Duration healthCheckRetryInterval) { this.clientFactory = requireNonNull(clientFactory, "clientFactory"); this.delegate = requireNonNull(delegate, "delegate"); this.healthCheckRetryInterval = requireNonNull(healthCheckRetryInterval, "healthCheckRetryInterval"); } /** * Update healthy servers and start to schedule healthcheck. * A subclass being initialized with this constructor must call {@link #init()} before start being used. */ protected void init() { checkAndUpdateHealthyServers().join(); scheduleCheckAndUpdateHealthyServers(); } /** * Returns the {@link ClientFactory} that will process {@link EndpointHealthChecker}'s healthcheck requests. */ protected final ClientFactory clientFactory() { return clientFactory; } private void scheduleCheckAndUpdateHealthyServers() { clientFactory.eventLoopGroup().schedule( () -> checkAndUpdateHealthyServers().thenRun(this::scheduleCheckAndUpdateHealthyServers), healthCheckRetryInterval.toMillis(), TimeUnit.MILLISECONDS); } private CompletableFuture<Void> checkAndUpdateHealthyServers() { List<ServerConnection> checkedServers = updateServerList(); CompletableFuture<List<Boolean>> healthCheckResults = CompletableFutures.successfulAsList( checkedServers.stream() .map(connection -> connection.healthChecker.isHealthy(connection.endpoint())) .collect(toImmutableList()), t -> false); return healthCheckResults.handle(voidFunction((result, thrown) -> { ImmutableList.Builder<Endpoint> newHealthyEndpoints = ImmutableList.builder(); for (int i = 0; i < result.size(); i++) { if (result.get(i)) { newHealthyEndpoints.add(checkedServers.get(i).endpoint()); } } setEndpoints(newHealthyEndpoints.build()); })); } /** * Update the servers this health checker client talks to. */ private List<ServerConnection> updateServerList() { Map<Endpoint, ServerConnection> allServersByEndpoint = allServers .stream() .collect(toImmutableMap(ServerConnection::endpoint, Function.identity())); return allServers = delegate .endpoints() .stream() .map(endpoint -> { ServerConnection connection = allServersByEndpoint.get(endpoint); if (connection != null) { return connection; } return new ServerConnection(endpoint, createEndpointHealthChecker(endpoint)); }) .collect(toImmutableList()); } /** * Creates a new {@link EndpointHealthChecker} instance that will check {@code endpoint} healthiness. */ protected abstract EndpointHealthChecker createEndpointHealthChecker(Endpoint endpoint); /** * Creates healthcheck {@link MetricSet} for this {@link HealthCheckedEndpointGroup}. */ public MetricSet newMetricSet(String metricName) { return new EndpointHealthStateGaugeSet(this, metricName); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("HealthCheckedEndpointGroup(all:["); for (ServerConnection connection : allServers) { buf.append(connection.endpoint).append(','); } buf.setCharAt(buf.length() - 1, ']'); buf.append(", healthy:["); for (Endpoint endpoint : endpoints()) { buf.append(endpoint).append(','); } buf.setCharAt(buf.length() - 1, ']'); buf.append(')'); return buf.toString(); } /** * Returns whether an {@link Endpoint} is healthy or not. */ @FunctionalInterface public interface EndpointHealthChecker { CompletableFuture<Boolean> isHealthy(Endpoint endpoint); } static final class ServerConnection { private final Endpoint endpoint; private final EndpointHealthChecker healthChecker; private ServerConnection(Endpoint endpoint, EndpointHealthChecker healthChecker) { this.endpoint = endpoint; this.healthChecker = healthChecker; } Endpoint endpoint() { return endpoint; } } }