org.apache.sling.discovery.impl.topology.connector.TopologyConnectorClient.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.discovery.impl.topology.connector.TopologyConnectorClient.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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 org.apache.sling.discovery.impl.topology.connector;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import java.util.Iterator;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.discovery.ClusterView;
import org.apache.sling.discovery.InstanceDescription;
import org.apache.sling.discovery.impl.Config;
import org.apache.sling.discovery.impl.cluster.ClusterViewService;
import org.apache.sling.discovery.impl.topology.announcement.Announcement;
import org.apache.sling.discovery.impl.topology.announcement.AnnouncementFilter;
import org.apache.sling.discovery.impl.topology.announcement.AnnouncementRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A topology connector client is used for sending (pinging) a remote topology
 * connector servlet and exchanging announcements with it
 */
public class TopologyConnectorClient implements TopologyConnectorClientInformation {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /** the endpoint url **/
    private final URL connectorUrl;

    /** the cluster view service **/
    private final ClusterViewService clusterViewService;

    /** the config service to user **/
    private final Config config;

    /** the id of this connection **/
    private final UUID id;

    /** the announcement registry **/
    private final AnnouncementRegistry announcementRegistry;

    /** the last inherited announcement **/
    private Announcement lastInheritedAnnouncement;

    /** the time when the last announcement was inherited - for webconsole use only **/
    private long lastPingedAt;

    /** the information about this server **/
    private final String serverInfo;

    /** the status code of the last post **/
    private int lastStatusCode = -1;

    /** SLING-3316: whether or not this connector was auto-stopped **/
    private boolean autoStopped = false;

    /** more details about connection failures **/
    private String statusDetails = null;

    /** SLING-2882: whether or not to suppress ping warnings **/
    private boolean suppressPingWarnings_ = false;

    private TopologyRequestValidator requestValidator;

    /** value of Content-Encoding of the last request **/
    private String lastRequestEncoding;

    /** value of Content-Encoding of the last repsonse **/
    private String lastResponseEncoding;

    /** SLING-3382: unix-time at which point the backoff-period ends and pings can be sent again **/
    private long backoffPeriodEnd = -1;

    TopologyConnectorClient(final ClusterViewService clusterViewService,
            final AnnouncementRegistry announcementRegistry, final Config config, final URL connectorUrl,
            final String serverInfo) {
        if (clusterViewService == null) {
            throw new IllegalArgumentException("clusterViewService must not be null");
        }
        if (announcementRegistry == null) {
            throw new IllegalArgumentException("announcementRegistry must not be null");
        }
        if (config == null) {
            throw new IllegalArgumentException("config must not be null");
        }
        if (connectorUrl == null) {
            throw new IllegalArgumentException("connectorUrl must not be null");
        }
        this.requestValidator = new TopologyRequestValidator(config);
        this.clusterViewService = clusterViewService;
        this.announcementRegistry = announcementRegistry;
        this.config = config;
        this.connectorUrl = connectorUrl;
        this.serverInfo = serverInfo;
        this.id = UUID.randomUUID();
    }

    /** ping the server and pass the announcements between the two **/
    void ping(final boolean force) {
        if (autoStopped) {
            // then we suppress any further pings!
            logger.debug("ping: autoStopped=true, hence suppressing any further pings.");
            return;
        }
        if (force) {
            backoffPeriodEnd = -1;
        } else if (backoffPeriodEnd > 0) {
            if (System.currentTimeMillis() < backoffPeriodEnd) {
                logger.debug("ping: not issueing a heartbeat due to backoff instruction from peer.");
                return;
            } else {
                logger.debug("ping: backoff period ended, issuing another ping now.");
            }
        }
        final String uri = connectorUrl.toString() + "." + clusterViewService.getSlingId() + ".json";
        if (logger.isDebugEnabled()) {
            logger.debug("ping: connectorUrl=" + connectorUrl + ", complete uri=" + uri);
        }
        HttpClient httpClient = new HttpClient();
        final PutMethod method = new PutMethod(uri);
        Announcement resultingAnnouncement = null;
        try {
            String userInfo = connectorUrl.getUserInfo();
            if (userInfo != null) {
                Credentials c = new UsernamePasswordCredentials(userInfo);
                httpClient.getState()
                        .setCredentials(new AuthScope(method.getURI().getHost(), method.getURI().getPort()), c);
            }

            Announcement topologyAnnouncement = new Announcement(clusterViewService.getSlingId());
            topologyAnnouncement.setServerInfo(serverInfo);
            final ClusterView clusterView = clusterViewService.getClusterView();
            topologyAnnouncement.setLocalCluster(clusterView);
            if (force) {
                logger.debug("ping: sending a resetBackoff");
                topologyAnnouncement.setResetBackoff(true);
            }
            announcementRegistry.addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() {

                public boolean accept(final String receivingSlingId, final Announcement announcement) {
                    // filter out announcements that are of old cluster instances
                    // which I dont really have in my cluster view at the moment
                    final Iterator<InstanceDescription> it = clusterViewService.getClusterView().getInstances()
                            .iterator();
                    while (it.hasNext()) {
                        final InstanceDescription instance = it.next();
                        if (instance.getSlingId().equals(receivingSlingId)) {
                            // then I have the receiving instance in my cluster view
                            // all fine then
                            return true;
                        }
                    }
                    // looks like I dont have the receiving instance in my cluster view
                    // then I should also not propagate that announcement anywhere
                    return false;
                }
            });
            final String p = requestValidator.encodeMessage(topologyAnnouncement.asJSON());

            if (logger.isDebugEnabled()) {
                logger.debug("ping: topologyAnnouncement json is: " + p);
            }
            requestValidator.trustMessage(method, p);
            if (config.isGzipConnectorRequestsEnabled()) {
                // tell the server that the content is gzipped:
                method.addRequestHeader("Content-Encoding", "gzip");
                // and gzip the body:
                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                final GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
                gzipOut.write(p.getBytes("UTF-8"));
                gzipOut.close();
                final byte[] gzippedEncodedJson = baos.toByteArray();
                method.setRequestEntity(new ByteArrayRequestEntity(gzippedEncodedJson, "application/json"));
                lastRequestEncoding = "gzip";
            } else {
                // otherwise plaintext:
                method.setRequestEntity(new StringRequestEntity(p, "application/json", "UTF-8"));
                lastRequestEncoding = "plaintext";
            }
            // independent of request-gzipping, we do accept the response to be gzipped,
            // so indicate this to the server:
            method.addRequestHeader("Accept-Encoding", "gzip");
            DefaultHttpMethodRetryHandler retryhandler = new DefaultHttpMethodRetryHandler(0, false);
            httpClient.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, retryhandler);
            httpClient.getHttpConnectionManager().getParams()
                    .setConnectionTimeout(1000 * config.getConnectionTimeout());
            httpClient.getHttpConnectionManager().getParams().setSoTimeout(1000 * config.getSoTimeout());
            method.getParams().setSoTimeout(1000 * config.getSoTimeout());
            httpClient.executeMethod(method);
            if (logger.isDebugEnabled()) {
                logger.debug("ping: done. code=" + method.getStatusCode() + " - " + method.getStatusText());
            }
            lastStatusCode = method.getStatusCode();
            lastResponseEncoding = null;
            if (method.getStatusCode() == HttpServletResponse.SC_OK) {
                final Header contentEncoding = method.getResponseHeader("Content-Encoding");
                if (contentEncoding != null && contentEncoding.getValue() != null
                        && contentEncoding.getValue().contains("gzip")) {
                    lastResponseEncoding = "gzip";
                } else {
                    lastResponseEncoding = "plaintext";
                }
                String responseBody = requestValidator.decodeMessage(method); // limiting to 16MB, should be way enough
                if (logger.isDebugEnabled()) {
                    logger.debug("ping: response body=" + responseBody);
                }
                if (responseBody != null && responseBody.length() > 0) {
                    Announcement inheritedAnnouncement = Announcement.fromJSON(responseBody);
                    final long backoffInterval = inheritedAnnouncement.getBackoffInterval();
                    if (backoffInterval > 0) {
                        // then reset the backoffPeriodEnd:

                        /* minus 1 sec to avoid slipping the interval by a few millis */
                        this.backoffPeriodEnd = System.currentTimeMillis() + (1000 * backoffInterval) - 1000;
                        logger.debug("ping: servlet instructed to backoff: backoffInterval=" + backoffInterval
                                + ", resulting in period end of " + new Date(backoffPeriodEnd));
                    } else {
                        logger.debug("ping: servlet did not instruct any backoff-ing at this stage");
                        this.backoffPeriodEnd = -1;
                    }
                    if (inheritedAnnouncement.isLoop()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug(
                                    "ping: connector response indicated a loop detected. not registering this announcement from "
                                            + inheritedAnnouncement.getOwnerId());
                        }
                        if (inheritedAnnouncement.getOwnerId().equals(clusterViewService.getSlingId())) {
                            // SLING-3316 : local-loop detected. Check config to see if we should stop this connector

                            if (config.isAutoStopLocalLoopEnabled()) {
                                inheritedAnnouncement = null; // results in connected -> false and representsloop -> true
                                autoStopped = true; // results in isAutoStopped -> true
                            }
                        }
                    } else {
                        inheritedAnnouncement.setInherited(true);
                        if (announcementRegistry.registerAnnouncement(inheritedAnnouncement) == -1) {
                            if (logger.isDebugEnabled()) {
                                logger.debug(
                                        "ping: connector response is from an instance which I already see in my topology"
                                                + inheritedAnnouncement);
                            }
                            statusDetails = "receiving side is seeing me via another path (connector or cluster) already (loop)";
                            return;
                        }
                    }
                    resultingAnnouncement = inheritedAnnouncement;
                    statusDetails = null;
                } else {
                    statusDetails = "no response body received";
                }
            } else {
                statusDetails = "got HTTP Status-Code: " + lastStatusCode;
            }
            // SLING-2882 : reset suppressPingWarnings_ flag in success case
            suppressPingWarnings_ = false;
        } catch (URIException e) {
            logger.warn("ping: Got URIException: " + e + ", uri=" + uri);
            statusDetails = e.toString();
        } catch (IOException e) {
            // SLING-2882 : set/check the suppressPingWarnings_ flag
            if (suppressPingWarnings_) {
                if (logger.isDebugEnabled()) {
                    logger.debug("ping: got IOException: " + e + ", uri=" + uri);
                }
            } else {
                suppressPingWarnings_ = true;
                logger.warn("ping: got IOException [suppressing further warns]: " + e + ", uri=" + uri);
            }
            statusDetails = e.toString();
        } catch (JSONException e) {
            logger.warn("ping: got JSONException: " + e);
            statusDetails = e.toString();
        } catch (RuntimeException re) {
            logger.warn("ping: got RuntimeException: " + re, re);
            statusDetails = re.toString();
        } finally {
            method.releaseConnection();
            lastInheritedAnnouncement = resultingAnnouncement;
            lastPingedAt = System.currentTimeMillis();
        }
    }

    public int getStatusCode() {
        return lastStatusCode;
    }

    public URL getConnectorUrl() {
        return connectorUrl;
    }

    public boolean representsLoop() {
        if (autoStopped) {
            return true;
        }
        if (lastInheritedAnnouncement == null) {
            return false;
        } else {
            return lastInheritedAnnouncement.isLoop();
        }
    }

    public boolean isConnected() {
        if (autoStopped) {
            return false;
        }
        if (lastInheritedAnnouncement == null) {
            return false;
        } else {
            return announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId());
        }
    }

    public String getStatusDetails() {
        if (autoStopped) {
            return "auto-stopped";
        }
        if (lastInheritedAnnouncement == null) {
            return statusDetails;
        } else {
            if (announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId())) {
                // still active - so no status details
                return null;
            } else {
                return "received announcement has expired (it was last renewed " + new Date(lastPingedAt)
                        + ") - consider increasing heartbeat timeout";
            }
        }
    }

    public long getLastHeartbeatSent() {
        return lastPingedAt;
    }

    public int getNextHeartbeatDue() {
        final long absDue;
        if (backoffPeriodEnd > 0) {
            absDue = backoffPeriodEnd;
        } else {
            absDue = lastPingedAt + 1000 * config.getHeartbeatInterval();
        }
        final int relDue = (int) ((absDue - System.currentTimeMillis()) / 1000);
        if (relDue < 0) {
            return -1;
        } else {
            return relDue;
        }
    }

    public boolean isAutoStopped() {
        return autoStopped;
    }

    public String getLastRequestEncoding() {
        return lastRequestEncoding == null ? "" : lastRequestEncoding;
    }

    public String getLastResponseEncoding() {
        return lastResponseEncoding == null ? "" : lastResponseEncoding;
    }

    public String getRemoteSlingId() {
        if (lastInheritedAnnouncement == null) {
            return null;
        } else {
            return lastInheritedAnnouncement.getOwnerId();
        }
    }

    public String getId() {
        return id.toString();
    }

    /** Disconnect this connector **/
    public void disconnect() {
        final String uri = connectorUrl.toString() + "." + clusterViewService.getSlingId() + ".json";
        if (logger.isDebugEnabled()) {
            logger.debug("disconnect: connectorUrl=" + connectorUrl + ", complete uri=" + uri);
        }

        if (lastInheritedAnnouncement != null) {
            announcementRegistry.unregisterAnnouncement(lastInheritedAnnouncement.getOwnerId());
        }

        HttpClient httpClient = new HttpClient();
        final DeleteMethod method = new DeleteMethod(uri);

        try {
            String userInfo = connectorUrl.getUserInfo();
            if (userInfo != null) {
                Credentials c = new UsernamePasswordCredentials(userInfo);
                httpClient.getState()
                        .setCredentials(new AuthScope(method.getURI().getHost(), method.getURI().getPort()), c);
            }

            requestValidator.trustMessage(method, null);
            httpClient.executeMethod(method);
            if (logger.isDebugEnabled()) {
                logger.debug("disconnect: done. code=" + method.getStatusCode() + " - " + method.getStatusText());
            }
            // ignoring the actual statuscode though as there's little we can
            // do about it after this point
        } catch (URIException e) {
            logger.warn("disconnect: Got URIException: " + e);
        } catch (IOException e) {
            logger.warn("disconnect: got IOException: " + e);
        } catch (RuntimeException re) {
            logger.error("disconnect: got RuntimeException: " + re, re);
        } finally {
            method.releaseConnection();
        }
    }
}