com.vmware.admiral.compute.container.HealthChecker.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.admiral.compute.container.HealthChecker.java

Source

/*
 * Copyright (c) 2016 VMware, Inc. All Rights Reserved.
 *
 * This product is licensed to you under the Apache License, Version 2.0 (the "License").
 * You may not use this product except in compliance with the License.
 *
 * This product may include a number of subcomponents with separate copyright notices
 * and license terms. Your use of these subcomponents is subject to the terms and
 * conditions of the subcomponent's license, as noted in the LICENSE file.
 */

package com.vmware.admiral.compute.container;

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;

import com.fasterxml.jackson.annotation.JsonProperty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import com.vmware.admiral.common.ManagementUriParts;
import com.vmware.admiral.common.util.QueryUtil;
import com.vmware.admiral.common.util.ServiceDocumentQuery;
import com.vmware.admiral.common.util.ServiceUtils;
import com.vmware.admiral.common.util.UriUtilsExtended;
import com.vmware.admiral.compute.container.ContainerDescriptionService.ContainerDescription;
import com.vmware.admiral.compute.container.ContainerService.ContainerState;
import com.vmware.admiral.compute.container.ContainerService.ContainerState.PowerState;
import com.vmware.admiral.compute.container.HealthChecker.HealthConfig.HttpVersion;
import com.vmware.admiral.compute.container.ShellContainerExecutorService.ShellContainerExecutorState;
import com.vmware.admiral.compute.container.maintenance.ContainerStats;
import com.vmware.photon.controller.model.resources.ComputeService.ComputeState;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.Service;
import com.vmware.xenon.common.Service.Action;
import com.vmware.xenon.common.ServiceHost;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.services.common.QueryTask;

/**
 * Describes a container instance. The same description service instance can be re-used across many
 * container instances acting as a shared template.
 */

public class HealthChecker {

    public static class HealthConfig {

        public static enum RequestProtocol {
            HTTP, TCP, COMMAND
        }

        public static enum HttpVersion {
            HTTP_v1_1, HTTP_v2
        }

        public static final String FIELD_NAME_PORT = "port";
        public static final String FIELD_NAME_URL = "url";
        public static final String FIELD_NAME_METHOD = "method";

        public RequestProtocol protocol;

        public Integer port;

        @JsonProperty("url_path")
        public String urlPath;

        @JsonProperty("http_version")
        public HttpVersion httpVersion;

        @JsonProperty("http_method")
        public Action httpMethod;

        @JsonProperty("timeout_millis")
        public Integer timeoutMillis;

        @JsonProperty("healthy_threshold")
        public Integer healthyThreshold;

        @JsonProperty("unhealthy_threshold")
        public Integer unhealthyThreshold;

        @JsonProperty("continue_provisioning_on_error")
        public boolean continueProvisioningOnError;

        public String command;
    }

    private final ServiceHost host;

    public HealthChecker(ServiceHost host) {
        this.host = host;
    }

    public void doHealthCheck(URI healthConfigLink) {
        doHealthCheck(healthConfigLink, null);
    }

    public void doHealthCheck(URI healthConfigLink, Consumer<ContainerStats> callback) {
        host.sendRequest(
                Operation.createGet(healthConfigLink).setReferer(host.getPublicUri()).setCompletion((o, ex) -> {
                    if (ex != null) {
                        host.log(Level.WARNING, "Failed to fetch self state for periodic maintenance: %s",
                                o.getUri());
                    } else {
                        ContainerDescription containerDescription = o.getBody(ContainerDescription.class);
                        processContainerHealth(containerDescription, callback);
                    }
                }));

    }

    public void doHealthCheckRequest(ContainerState containerState, HealthConfig healthConfig,
            Consumer<ContainerStats> callback) {
        if (healthConfig == null || healthConfig.protocol == null) {
            return;
        }
        if (containerState.powerState == PowerState.PAUSED || containerState.powerState == PowerState.RETIRED
                || containerState.powerState == PowerState.PROVISIONING
                || containerState.powerState == PowerState.STOPPED) {

            if (callback != null) {
                callback.accept(null);
            }

            return;
        }

        switch (healthConfig.protocol) {
        case HTTP:
            healthCheckHttp(containerState, healthConfig, null, callback);
            break;
        case TCP:
            healthCheckTcp(containerState, healthConfig, null, callback);
            break;
        case COMMAND:
            healthCheckExec(containerState, healthConfig, callback);
            break;
        default:
            host.log(Level.WARNING, "Health config protocol not supported: %s", healthConfig.protocol);
            break;
        }
    }

    private void processContainerHealth(ContainerDescription containerDescription,
            Consumer<ContainerStats> callback) {

        QueryTask compositeQueryTask = QueryUtil.buildQuery(ContainerState.class, true);

        QueryUtil.addExpandOption(compositeQueryTask);

        String containerDescriptionLink = UriUtils.buildUriPath(ManagementUriParts.CONTAINER_DESC,
                Service.getId(containerDescription.documentSelfLink));
        QueryUtil.addListValueClause(compositeQueryTask, ContainerState.FIELD_NAME_DESCRIPTION_LINK,
                Arrays.asList(containerDescriptionLink));

        new ServiceDocumentQuery<ContainerState>(host, ContainerState.class).query(compositeQueryTask, (r) -> {
            if (r.hasException()) {
                host.log(Level.SEVERE, "Failed to retrieve container's health config: %s - %s",
                        r.getDocumentSelfLink(), r.getException());
            } else if (r.hasResult()) {
                doHealthCheckRequest(r.getResult(), containerDescription.healthConfig, callback);
            }
        });
    }

    private void healthCheckExec(ContainerState containerState, HealthConfig healthConfig,
            Consumer<ContainerStats> callback) {

        ShellContainerExecutorState executorState = new ShellContainerExecutorState();
        executorState.command = healthConfig.command.split(" ");
        executorState.attachStdOut = false;

        URI executeUri = UriUtils.buildUri(host, ShellContainerExecutorService.SELF_LINK);

        executeUri = UriUtils.extendUriWithQuery(executeUri, ShellContainerExecutorService.CONTAINER_LINK_URI_PARAM,
                containerState.documentSelfLink);

        host.sendRequest(Operation.createPost(executeUri).setReferer(host.getPublicUri()).setBody(executorState)
                .setCompletion((o, e) -> {
                    String body = o.getBody(String.class);
                    if (e == null && body != null && !body.isEmpty()) {
                        // We have requested stderr only
                        e = new RuntimeException(String.format("Health check failed: %s", o.getBody(String.class)));
                    }
                    handleHealthResponse(containerState, e, callback);
                }));

    }

    private void healthCheckTcp(ContainerState containerState, HealthConfig healthConfig, String[] hostPortBindings,
            Consumer<ContainerStats> callback) {
        if (hostPortBindings == null) {
            determineContainerHostPort(containerState, healthConfig,
                    (bindings) -> healthCheckTcp(containerState, healthConfig, bindings, callback));
            return;
        }

        Integer configPort = Integer.valueOf(hostPortBindings[1]);
        int port = configPort > 0 ? configPort : 80;
        Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class)
                .remoteAddress(new InetSocketAddress(hostPortBindings[0], port))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, healthConfig.timeoutMillis)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel arg0) throws Exception {
                        // Nothing to setup
                    }
                });

        ChannelFuture channelFuture = bootstrap.connect();
        channelFuture.addListener(new ChannelFutureListener() {

            @Override
            public void operationComplete(ChannelFuture result) throws Exception {
                handleHealthResponse(containerState, result.cause(), callback);
                result.channel().close();
            }

        });
    }

    private void healthCheckHttp(ContainerState containerState, HealthConfig healthConfig,
            String[] hostPortBindings, Consumer<ContainerStats> callback) {

        if (hostPortBindings == null) {
            determineContainerHostPort(containerState, healthConfig,
                    (bindings) -> healthCheckHttp(containerState, healthConfig, bindings, callback));
            return;
        }

        if (healthConfig.urlPath == null || healthConfig.urlPath.isEmpty()) {
            healthConfig.urlPath = "/";
        }

        URI uri = null;
        try {
            if (!healthConfig.urlPath.startsWith("/")) {
                healthConfig.urlPath = "/" + healthConfig.urlPath;
            }

            if (healthConfig.port != null && healthConfig.port > 0) {
                uri = new URI(UriUtils.HTTP_SCHEME, null, hostPortBindings[0],
                        Integer.parseInt(hostPortBindings[1]), healthConfig.urlPath, null, null);
            } else {
                uri = new URI(UriUtils.HTTP_SCHEME, hostPortBindings[0], healthConfig.urlPath, null);
            }
        } catch (URISyntaxException e) {
            host.log(Level.WARNING, "Health config for container description %s is invalid: %s",
                    containerState.descriptionLink, e);
            return;
        }

        // with createGet the authorization context is propagated to the completion handler...
        Operation op = Operation.createGet(uri).setAction(healthConfig.httpMethod).setReferer(host.getPublicUri())
                .setCompletion((o, ex) -> {
                    handleHealthResponse(containerState, ex, callback);
                });

        if (healthConfig.httpVersion == HttpVersion.HTTP_v2) {
            op.setConnectionSharing(true);
        }

        if (healthConfig.timeoutMillis != null && healthConfig.timeoutMillis > 0) {
            op.setExpiration(ServiceUtils
                    .getExpirationTimeFromNowInMicros(TimeUnit.MILLISECONDS.toMicros(healthConfig.timeoutMillis)));
        }

        host.sendRequest(op);
    }

    private void determineContainerHostPort(ContainerState containerState, HealthConfig healthConfig,
            Consumer<String[]> callback) {

        if (containerState.ports != null) {
            for (PortBinding portBinding : containerState.ports) {
                if (portBinding.hostPort != null && portBinding.containerPort != null
                        && !portBinding.hostPort.isEmpty()
                        && Integer.parseInt(portBinding.containerPort) == healthConfig.port) {
                    getHostPortBinding(containerState, portBinding.hostPort, null, callback);
                    return;
                }
            }
        }
        host.log(Level.WARNING, "Container does not expose ports - using container address as public");
        callback.accept(new String[] { containerState.address, String.valueOf(healthConfig.port) });
    }

    public void getHostPortBinding(ContainerState containerState, String port, String hostAddress,
            Consumer<String[]> callback) {
        if (hostAddress == null || hostAddress.isEmpty()) {
            getContainerHost(containerState.parentLink,
                    (host) -> getHostPortBinding(containerState, port, host.address, callback));
            return;
        }

        callback.accept(new String[] { UriUtilsExtended.extractHost(hostAddress), port });
    }

    private void getContainerHost(String parentLink, Consumer<ComputeState> callback) {
        host.sendRequest(Operation.createGet(UriUtils.buildUri(host, parentLink)).setReferer(host.getPublicUri())
                .setCompletion((ob, ex) -> {
                    if (ex != null) {
                        host.log(Level.SEVERE, "Unable to retrieve container's host during health check: %s", ex);
                    } else {
                        callback.accept(ob.getBody(ComputeState.class));
                    }
                }));

    }

    private void handleHealthResponse(ContainerState containerState, Throwable ex,
            Consumer<ContainerStats> callback) {
        if (ex != null) {
            host.log(Level.WARNING, "Health check status is failed for container %s : %s", containerState, ex);

        }

        /* if ex != null, the health check is failed */
        ContainerStats containerStats = new ContainerStats();
        containerStats.healthCheckSuccess = (ex == null);
        containerStats.containerStopped = containerState.powerState == PowerState.STOPPED;
        URI uri = UriUtils.buildUri(host, containerState.documentSelfLink);
        host.sendRequest(Operation.createPatch(uri).setBody(containerStats).setReferer(host.getPublicUri())
                .setCompletion((ob, exception) -> {
                    if (exception != null) {
                        host.log(Level.WARNING, "Failed to patch health status on periodic maintenance: %s",
                                containerState.documentSelfLink);

                        if (callback != null) {
                            callback.accept(null);
                        }

                        return;
                    }
                    ContainerStats stats = ob.getBody(ContainerStats.class);
                    if (callback != null) {
                        callback.accept(stats);
                    }
                }));
    }
}