You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by st...@apache.org on 2015/10/21 17:50:37 UTC

svn commit: r1709867 [2/3] - in /sling/trunk/bundles/extensions/discovery/oak: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/sling/ src/main/java/org/apache/sling/discovery/ src/main/java/org/apa...

Added: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/TopologyWebConsolePlugin.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/TopologyWebConsolePlugin.java?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/TopologyWebConsolePlugin.java (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/TopologyWebConsolePlugin.java Wed Oct 21 15:50:37 2015
@@ -0,0 +1,1048 @@
+/*
+ * 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.oak;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.announcement.CachedAnnouncement;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorClientInformation;
+import org.apache.sling.discovery.commons.providers.spi.base.DiscoveryLiteDescriptor;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.TopologyView;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Simple webconsole which gives an overview of the topology visible by the
+ * discovery service
+ */
+@Component
+@Service(value = { TopologyEventListener.class, Servlet.class })
+@Properties({
+    @Property(name=org.osgi.framework.Constants.SERVICE_DESCRIPTION,
+            value="Apache Sling Web Console Plugin to display Background servlets and ExecutionEngine status"),
+    @Property(name=WebConsoleConstants.PLUGIN_LABEL, value=TopologyWebConsolePlugin.LABEL),
+    @Property(name=WebConsoleConstants.PLUGIN_TITLE, value=TopologyWebConsolePlugin.TITLE),
+    @Property(name="felix.webconsole.configprinter.modes", value={"zip"})
+})
+@SuppressWarnings("serial")
+public class TopologyWebConsolePlugin extends AbstractWebConsolePlugin implements TopologyEventListener {
+
+    public static final String LABEL = "topology";
+    public static final String TITLE = "Topology Management";
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** the truncated log of topology events, filtered by property change types. shown in webconsole **/
+    private final List<String> propertyChangeLog = new LinkedList<String>();
+
+    /** the truncated log of topology events, shown in webconsole **/
+    private final List<String> topologyLog = new LinkedList<String>();
+
+    /** the date format used in the truncated log of topology events **/
+    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+
+    @Reference
+    private ClusterViewService clusterViewService;
+
+    @Reference
+    private AnnouncementRegistry announcementRegistry;
+
+    @Reference
+    private ConnectorRegistry connectorRegistry;
+
+    @Reference
+    protected ResourceResolverFactory resourceResolverFactory;
+
+    private TopologyView currentView;
+    
+    private List<String> discoveryLiteHistory = new LinkedList<String>();
+
+    @Override
+    public String getLabel() {
+        return LABEL;
+    }
+
+    @Override
+    public String getTitle() {
+        return TITLE;
+    }
+
+    @Activate
+    @Override
+    public void activate(final BundleContext bundleContext) {
+        super.activate(bundleContext);
+    }
+
+    @Deactivate
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    protected void renderContent(final HttpServletRequest req, final HttpServletResponse res)
+            throws ServletException, IOException {
+        Object rawRoot = req.getAttribute(WebConsoleConstants.ATTR_PLUGIN_ROOT);
+        if (!(rawRoot instanceof String)) {
+            throw new ServletException("Illegal attr: "
+                    + WebConsoleConstants.ATTR_PLUGIN_ROOT);
+        }
+
+        String root = rawRoot.toString();
+        String pathInfo = req.getRequestURI().substring(root.length());
+
+        final PrintWriter pw = res.getWriter();
+
+        if (pathInfo.equals("")) {
+            if ( this.currentView != null ) {
+                renderOverview(pw,  currentView);
+            } else {
+                pw.println("<p class=\"statline ui-state-highlight\">No view available</p>");
+                pw.println("<br/>");
+                pw.println("No TOPOLOGY_INIT received yet, therefore no view available yet.");
+            }
+        } else {
+            StringTokenizer st = new StringTokenizer(pathInfo, "/");
+            final String nodeId = st.nextToken();
+            renderProperties(pw, req.getContextPath(), nodeId);
+        }
+    }
+
+    /**
+     * Render the properties page of a particular instance
+     */
+    private void renderProperties(final PrintWriter pw, final String contextPath, final String nodeId) {
+    	if (logger.isDebugEnabled()) {
+    		logger.debug("renderProperties: nodeId=" + nodeId);
+    	}
+        final TopologyView tv = this.currentView;
+        @SuppressWarnings("unchecked")
+        Set<InstanceDescription> instances = ( tv == null ? (Set<InstanceDescription>)Collections.EMPTY_SET :
+
+                tv.findInstances(new InstanceFilter() {
+
+                    public boolean accept(InstanceDescription instance) {
+                        String slingId = instance.getSlingId();
+                    	if (logger.isDebugEnabled()) {
+	                        logger.debug("renderProperties/picks: slingId={}", slingId);
+                    	}
+                        return (slingId.equals(nodeId));
+                    }
+                }));
+
+        if (instances != null && instances.size() == 1) {
+            InstanceDescription instance = instances.iterator().next();
+            pw.println("Properties of " + instance.getSlingId() + ":<br/>");
+
+            pw.println("<table class=\"adapters nicetable ui-widget tablesorter\">");
+            pw.println("<thead>");
+            pw.println("<tr>");
+            pw.println("<th class=\"header ui-widget-header\">Key</th>");
+            pw.println("<th class=\"header ui-widget-header\">Value</th>");
+            pw.println("</tr>");
+            pw.println("</thead>");
+            pw.println("<tbody>");
+            boolean odd = true;
+            for (Iterator<Entry<String, String>> it = instance.getProperties()
+                    .entrySet().iterator(); it.hasNext();) {
+                Entry<String, String> entry = it.next();
+                String oddEven = odd ? "odd" : "even";
+                odd = !odd;
+                pw.println("<tr class=\"" + oddEven + " ui-state-default\">");
+
+                pw.println("<td>" + entry.getKey() + "</td>");
+                pw.println("<td>" + entry.getValue() + "</td>");
+
+                pw.println("</tr>");
+            }
+            pw.println("</tbody>");
+            pw.println("</table>");
+        }
+    }
+
+    protected ResourceResolver getResourceResolver() throws LoginException {
+        return resourceResolverFactory.getAdministrativeResourceResolver(null);
+    }
+
+    /**
+     * Render the overview of the entire topology
+     */
+    private void renderOverview(final PrintWriter pw, final TopologyView topology) {
+        pw.println("<p class=\"statline ui-state-highlight\">Configuration</p>");
+        pw.println("<br/>");
+        pw.print("<a href=\"${appRoot}/configMgr/org.apache.sling.discovery.oak.Config\">Configure Discovery.Oak Service</a>");
+        pw.println("<br/>");
+        pw.println("<br/>");
+        final String changing;
+        if (!topology.isCurrent()) {
+        	changing = " <b><i>CHANGING!</i> (the view is no longer current!)</b>";
+        } else {
+        	changing = "";
+        }
+        pw.println("<p class=\"statline ui-state-highlight\">Topology"+changing+"</p>");
+        pw.println("<div class=\"ui-widget-header ui-corner-top buttonGroup\" style=\"height: 15px;\">");
+        pw.println("<span style=\"float: left; margin-left: 1em;\">Instances in the topology</span>");
+        pw.println("</div>");
+        pw.println("<table class=\"adapters nicetable ui-widget tablesorter\">");
+        pw.println("<thead>");
+        pw.println("<tr>");
+        pw.println("<th class=\"header ui-widget-header\">Sling id (click for properties)</th>");
+        pw.println("<th class=\"header ui-widget-header\">ClusterView id</th>");
+        pw.println("<th class=\"header ui-widget-header\">Local instance</th>");
+        pw.println("<th class=\"header ui-widget-header\">Leader instance</th>");
+        pw.println("<th class=\"header ui-widget-header\">In local cluster</th>");
+        pw.println("<th class=\"header ui-widget-header\">Announced by instance</th>");
+        pw.println("</tr>");
+        pw.println("</thead>");
+        pw.println("<tbody>");
+
+        Set<ClusterView> clusters = topology.getClusterViews();
+        ClusterView myCluster = topology.getLocalInstance().getClusterView();
+        boolean odd = true;
+        renderCluster(pw, myCluster, myCluster, odd, topology.isCurrent());
+
+        for (Iterator<ClusterView> it = clusters.iterator(); it.hasNext();) {
+            ClusterView clusterView = it.next();
+            if (clusterView.equals(myCluster)) {
+                // skip - I already rendered that
+                continue;
+            }
+            odd = !odd;
+            renderCluster(pw, clusterView, myCluster, odd, topology.isCurrent());
+        }
+
+        pw.println("</tbody>");
+        pw.println("</table>");
+
+        pw.println("<br/>");
+        pw.println("<br/>");
+        pw.println("<p class=\"statline ui-state-highlight\">Connectors</p>");
+        listIncomingTopologyConnectors(pw);
+        listOutgoingTopologyConnectors(pw);
+        pw.println("<br/>");
+
+        ResourceResolver resourceResolver = null;
+        pw.println("<p class=\"statline ui-state-highlight\">Discovery-Lite Descriptor History</p>");
+        pw.println("<pre>");
+        for (String discoLiteHistoryEntry : discoveryLiteHistory) {
+            pw.println(discoLiteHistoryEntry);
+        }
+        pw.println("</pre>");
+        pw.println("<br/>");
+        pw.println("<p class=\"statline ui-state-highlight\">Current Discovery-Lite Descriptor Value</p>");
+        pw.println("<pre>");
+        try{
+            resourceResolver = getResourceResolver();
+            DiscoveryLiteDescriptor descriptor = DiscoveryLiteDescriptor.getDescriptorFrom(resourceResolver);
+            final String logEntry = getCurrentDateFormatted() + ": " + descriptor.getDescriptorStr();
+            pw.println(logEntry);
+        } catch(Exception e) {
+            logger.error("renderOverview: Exception: "+e, e);
+            pw.println("Got exception trying to get repository descriptor: "+e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+        pw.println("</pre>");
+        pw.println("<br/>");
+        
+        pw.println("<p class=\"statline ui-state-highlight\">Topology Change History</p>");
+        pw.println("<pre>");
+        for (Iterator<String> it = topologyLog
+                .iterator(); it.hasNext();) {
+            String aLogEntry = it.next();
+            pw.println(aLogEntry);
+        }
+        pw.println("</pre>");
+        pw.println("<br/>");
+        pw.println("<p class=\"statline ui-state-highlight\">Property Change History</p>");
+        pw.println("<pre>");
+        for (Iterator<String> it = propertyChangeLog
+                .iterator(); it.hasNext();) {
+            String aLogEntry = it.next();
+            pw.println(aLogEntry);
+        }
+        pw.println("</pre>");
+        pw.println("</br>");
+    }
+
+    /**
+     * Render a particular cluster (into table rows)
+     */
+    private void renderCluster(final PrintWriter pw, final ClusterView renderCluster, final ClusterView localCluster, final boolean odd, final boolean current) {
+        final Collection<Announcement> announcements = announcementRegistry.listAnnouncementsInSameCluster(localCluster);
+
+        for (Iterator<InstanceDescription> it = renderCluster.getInstances()
+                .iterator(); it.hasNext();) {
+            final InstanceDescription instanceDescription = it.next();
+            final boolean inLocalCluster = renderCluster == localCluster;
+            Announcement parentAnnouncement = null;
+            for (Iterator<Announcement> it2 = announcements.iterator(); it2
+                    .hasNext();) {
+                Announcement announcement = it2.next();
+                for (Iterator<InstanceDescription> it3 = announcement
+                        .listInstances().iterator(); it3.hasNext();) {
+                    InstanceDescription announcedInstance = it3.next();
+                    if (announcedInstance.getSlingId().equals(
+                            instanceDescription.getSlingId())) {
+                        parentAnnouncement = announcement;
+                        break;
+                    }
+                }
+            }
+
+            final String oddEven = odd ? "odd" : "even";
+
+            if (current && (inLocalCluster || (parentAnnouncement!=null))) {
+                pw.println("<tr class=\"" + oddEven + " ui-state-default\">");
+            } else {
+                pw.println("<tr class=\"" + oddEven + " ui-state-error\">");
+            }
+            final boolean isLocal = instanceDescription.isLocal();
+            final String slingId = instanceDescription.getSlingId();
+            pw.print("<td>");
+            if ( isLocal) {
+                pw.print("<b>");
+            }
+            pw.print("<a href=\"");
+            pw.print(this.getLabel());
+            pw.print('/');
+            pw.print(slingId);
+            pw.print("\">");
+            pw.print(slingId);
+            pw.print("</a>");
+            if ( isLocal) {
+                pw.print("</b>");
+            }
+            pw.println("</td>");
+            pw.println("<td>"
+                    + (instanceDescription.getClusterView() == null ? "null"
+                            : instanceDescription.getClusterView().getId())
+                    + "</td>");
+            pw.println("<td>" + (isLocal ? "<b>true</b>" : "false") + "</td>");
+            pw.println("<td>"
+                    + (instanceDescription.isLeader() ? "<b>true</b>" : "false")
+                    + "</td>");
+            if (inLocalCluster) {
+                pw.println("<td>local</td>");
+                pw.println("<td>n/a</td>");
+            } else {
+                pw.println("<td>remote</td>");
+                if (parentAnnouncement != null) {
+                    pw.println("<td>" + parentAnnouncement.getOwnerId()
+                            + "</td>");
+                } else {
+                    pw.println("<td><b>(changing)</b></td>");
+                }
+            }
+            pw.println("</tr>");
+        }
+
+    }
+
+    /**
+     * Render the outgoing topology connectors - including the header-div and table
+     */
+    private void listOutgoingTopologyConnectors(final PrintWriter pw) {
+        boolean odd = false;
+        pw.println("<div class=\"ui-widget-header ui-corner-top buttonGroup\" style=\"height: 15px;\">");
+        pw.println("<span style=\"float: left; margin-left: 1em;\">Outgoing topology connectors</span>");
+        pw.println("</div>");
+        pw.println("<table class=\"adapters nicetable ui-widget tablesorter\">");
+        pw.println("<thead>");
+        pw.println("<tr>");
+        pw.println("<th class=\"header ui-widget-header\">Connector url</th>");
+        pw.println("<th class=\"header ui-widget-header\">Connected to slingId</th>");
+        pw.println("<th class=\"header ui-widget-header\">Connector status</th>");
+        pw.println("<th class=\"header ui-widget-header\">Last heartbeat&nbsp;</th>");
+        pw.println("<th class=\"header ui-widget-header\">Next heartbeat&nbsp;</th>");
+        pw.println("<th class=\"header ui-widget-header\">Request encoding&nbsp;</th>");
+        pw.println("<th class=\"header ui-widget-header\">Response encoding&nbsp;</th>");
+        // pw.println("<th class=\"header ui-widget-header\">Fallback connector urls</th>");
+        pw.println("</tr>");
+        pw.println("</thead>");
+        pw.println("<tbody>");
+
+        Collection<TopologyConnectorClientInformation> outgoingConnections = connectorRegistry
+                .listOutgoingConnectors();
+        for (Iterator<TopologyConnectorClientInformation> it = outgoingConnections
+                .iterator(); it.hasNext();) {
+            TopologyConnectorClientInformation topologyConnectorClient = it
+                    .next();
+            final String oddEven = odd ? "odd" : "even";
+            odd = !odd;
+            final String remoteSlingId = topologyConnectorClient.getRemoteSlingId();
+            final boolean isConnected = topologyConnectorClient.isConnected() && remoteSlingId != null;
+            final boolean autoStopped = topologyConnectorClient.isAutoStopped();
+            final boolean representsLoop = topologyConnectorClient.representsLoop();
+            if (isConnected || autoStopped || representsLoop) {
+                pw.println("<tr class=\"" + oddEven + " ui-state-default\">");
+            } else {
+                pw.println("<tr class=\"" + oddEven + " ui-state-error\">");
+            }
+            pw.println("<td>"
+                    + topologyConnectorClient.getConnectorUrl().toString()
+                    + "</td>");
+            if (autoStopped) {
+            	pw.println("<td><b>auto-stopped</b></td>");
+            	pw.println("<td><b>auto-stopped due to local-loop</b></td>");
+            } else if (isConnected && !representsLoop) {
+                pw.println("<td>" + remoteSlingId + "</td>");
+                pw.println("<td>ok, in use</td>");
+            } else if (representsLoop) {
+                pw.println("<td>" + remoteSlingId + "</td>");
+                pw.println("<td>ok, unused (loop or duplicate): standby</td>");
+            } else {
+                final int statusCode = topologyConnectorClient.getStatusCode();
+                final String statusDetails = topologyConnectorClient.getStatusDetails();
+                final String tooltipText;
+                switch(statusCode) {
+                case HttpServletResponse.SC_UNAUTHORIZED:
+                    tooltipText = HttpServletResponse.SC_UNAUTHORIZED +
+                        ": possible setup issue of discovery.oak on target instance, or wrong URL";
+                    break;
+                case HttpServletResponse.SC_NOT_FOUND:
+                    tooltipText = HttpServletResponse.SC_NOT_FOUND +
+                        ": possible white list rejection by target instance";
+                    break;
+                case -1:
+                    tooltipText = "-1: check error log. possible connection refused.";
+                    break;
+                default:
+                    tooltipText = null;
+                }
+                final String tooltip = tooltipText==null ? "" : (" title=\""+tooltipText+"\"");
+                pw.println("<td><b>not connected</b></td>");
+                pw.println("<td"+tooltip+"><b>not ok (HTTP Status-Code: "+statusCode+", "+statusDetails+")</b></td>");
+            }
+            pw.println("<td>"+beautifiedTimeDiff(topologyConnectorClient.getLastPingSent())+"</td>");
+            pw.println("<td>"+beautifiedDueTime(topologyConnectorClient.getNextPingDue())+"</td>");
+            pw.println("<td>"+topologyConnectorClient.getLastRequestEncoding()+"</td>");
+            pw.println("<td>"+topologyConnectorClient.getLastResponseEncoding()+"</td>");
+            // //TODO fallback urls are not yet implemented!
+            // String fallbackConnectorUrls;
+            // List<String> urls = topologyConnectorClient
+            // .listFallbackConnectorUrls();
+            // if (urls == null || urls.size() == 0) {
+            // fallbackConnectorUrls = "n/a";
+            // } else {
+            // fallbackConnectorUrls = "";
+            // for (Iterator<String> it2 = urls.iterator(); it2.hasNext();) {
+            // String aFallbackConnectorUrl = it2.next();
+            // fallbackConnectorUrls = fallbackConnectorUrls
+            // + aFallbackConnectorUrl + "<br/>";
+            // }
+            // }
+            // pw.println("<td>" + fallbackConnectorUrls + "</td>");
+        }
+
+        pw.println("</tbody>");
+        pw.println("</table>");
+    }
+    
+    private String beautifiedDueTime(long secondsDue) {
+        if (secondsDue<-1) {
+            return "overdue";
+        } else if (secondsDue<=0) {
+            return "now-ish";
+        } else if (secondsDue==1) {
+            return "in 1 second";
+        } else {
+            int minsDue = (int) (secondsDue / 60);
+            if (minsDue<5) {
+                return "in "+secondsDue+" seconds";
+            } else {
+                return "in "+minsDue+" minutes";
+            }
+        }
+    }
+
+    private String beautifiedTimeDiff(long heartbeatTime) {
+        final long diff = System.currentTimeMillis() - heartbeatTime;
+        long seconds = (diff/1000);
+        if (heartbeatTime<=0) {
+            return "n/a";
+        } else if (seconds==0) {
+            return diff+" millis ago";
+        } else if (seconds==1) {
+            return "1 second ago";
+        } else if (seconds<300) {
+            // then print seconds
+            return seconds+" seconds ago";
+        } else {
+            // then print seconds
+            return (seconds/60)+" minute ago";
+        }
+    }
+
+    /**
+     * Render the incoming topology connectors - including the header-div and table
+     */
+    private void listIncomingTopologyConnectors(final PrintWriter pw) {
+        boolean odd = false;
+        pw.println("<div class=\"ui-widget-header ui-corner-top buttonGroup\" style=\"height: 15px;\">");
+        pw.println("<span style=\"float: left; margin-left: 1em;\">Incoming topology connectors</span>");
+        pw.println("</div>");
+        pw.println("<table class=\"adapters nicetable ui-widget tablesorter\">");
+        pw.println("<thead>");
+        pw.println("<tr>");
+        pw.println("<th class=\"header ui-widget-header\">Owner slingId</th>");
+        pw.println("<th class=\"header ui-widget-header\">Server info</th>");
+        pw.println("<th class=\"header ui-widget-header\">Last heartbeat</th>");
+        pw.println("<th class=\"header ui-widget-header\">Timeout</th>");
+        pw.println("</tr>");
+        pw.println("</thead>");
+        pw.println("<tbody>");
+
+        Collection<CachedAnnouncement> incomingConnections = announcementRegistry.listLocalIncomingAnnouncements();
+        for (Iterator<CachedAnnouncement> it = incomingConnections.iterator(); it
+                .hasNext();) {
+            CachedAnnouncement incomingCachedAnnouncement = it.next();
+            Announcement incomingAnnouncement = incomingCachedAnnouncement.getAnnouncement();
+            String oddEven = odd ? "odd" : "even";
+            odd = !odd;
+
+            pw.println("<tr class=\"" + oddEven + " ui-state-default\">");
+            pw.println("<td>" + incomingAnnouncement.getOwnerId() + "</td>");
+            if (incomingAnnouncement.getServerInfo() != null) {
+                pw.println("<td>" + incomingAnnouncement.getServerInfo()
+                        + "</td>");
+            } else {
+                pw.println("<td><i>n/a</i></td>");
+            }
+            pw.println("<td>"+beautifiedTimeDiff(incomingCachedAnnouncement.getLastPing())+"</td>");
+            pw.println("<td>"+beautifiedDueTime(incomingCachedAnnouncement.getSecondsUntilTimeout())+"</td>");
+
+            pw.println("</tr>");
+        }
+
+        pw.println("</tbody>");
+        pw.println("</table>");
+        pw.println("<br/>");
+        pw.println("<br/>");
+    }
+
+    /**
+     * keep a truncated history of the log events for information purpose (to be shown in the webconsole)
+     */
+    public void handleTopologyEvent(final TopologyEvent event) {
+        if (event.getType() == Type.PROPERTIES_CHANGED) {
+            this.currentView = event.getNewView();
+
+            Set<InstanceDescription> newInstances = event.getNewView()
+                    .getInstances();
+            StringBuilder sb = new StringBuilder();
+            for (Iterator<InstanceDescription> it = newInstances.iterator(); it
+                    .hasNext();) {
+                final InstanceDescription newInstanceDescription = it.next();
+                InstanceDescription oldInstanceDescription = findInstance(
+                        event.getOldView(), newInstanceDescription.getSlingId());
+                if (oldInstanceDescription == null) {
+                    logger.error("handleTopologyEvent: got a property changed but did not find instance "
+                            + newInstanceDescription
+                            + " in oldview.. event="
+                            + event);
+                    addEventLog(event.getType(), event.getType().toString());
+                    return;
+                }
+
+                Map<String, String> oldProps = oldInstanceDescription
+                        .getProperties();
+                Map<String, String> newProps = newInstanceDescription
+                        .getProperties();
+                StringBuilder diff = diff(oldProps, newProps);
+                if (diff.length() > 0) {
+                    if (sb.length() != 0) {
+                        sb.append(", ");
+                    }
+                    sb.append("on instance "
+                            + newInstanceDescription.getSlingId() + (newInstanceDescription.isLeader() ? " [isLeader]" : "") 
+                            + ": " + diff);
+                }
+            }
+
+            addEventLog(event.getType(), sb.toString());
+        } else if (event.getType() == Type.TOPOLOGY_INIT) {
+            this.currentView = event.getNewView();
+            StringBuilder details = new StringBuilder();
+            for (Iterator<InstanceDescription> it = event.getNewView()
+                    .getInstances().iterator(); it.hasNext();) {
+                InstanceDescription newInstance = it.next();
+                if (details.length() != 0) {
+                    details.append(", ");
+                }
+                details.append(newInstance.getSlingId());
+                if (newInstance.isLeader()) {
+                    details.append(" [isLeader]");
+                }
+            }
+            addEventLog(event.getType(),
+                    "view: " + shortViewInfo(event.getNewView()) + ". "
+                            + details);
+        } else if (event.getType() == Type.TOPOLOGY_CHANGING) {
+            this.currentView = event.getOldView();
+            addEventLog(event.getType(),
+                    "old view: " + shortViewInfo(event.getOldView()));
+        } else {
+            this.currentView = event.getNewView();
+            if (event.getOldView() == null) {
+                addEventLog(event.getType(),
+                        "new view: " + shortViewInfo(event.getNewView()));
+            } else {
+                StringBuilder details = new StringBuilder();
+                for (Iterator<InstanceDescription> it = event.getNewView()
+                        .getInstances().iterator(); it.hasNext();) {
+                    InstanceDescription newInstance = it.next();
+                    if (findInstance(event.getOldView(),
+                            newInstance.getSlingId()) == null) {
+                        if (details.length() != 0) {
+                            details.append(", ");
+                        }
+                        details.append(newInstance.getSlingId() + " joined");
+                    }
+                }
+                for (Iterator<InstanceDescription> it = event.getOldView()
+                        .getInstances().iterator(); it.hasNext();) {
+                    InstanceDescription oldInstance = it.next();
+                    if (findInstance(event.getNewView(),
+                            oldInstance.getSlingId()) == null) {
+                        if (details.length() != 0) {
+                            details.append(", ");
+                        }
+                        details.append(oldInstance.getSlingId() + " left");
+                    }
+                }
+                final InstanceDescription li = event.getNewView().getLocalInstance();
+                if (li!=null) {
+                    ClusterView clusterView = li.getClusterView();
+                    if (clusterView!=null) {
+                        final InstanceDescription leader = clusterView.getLeader();
+                        if (leader!=null) {
+                            if (details.length() !=0) {
+                                details.append(", ");
+                            }
+                            details.append("[isLeader: "+leader.getSlingId()+"]");
+                        }
+                    }
+                }
+    
+                addEventLog(
+                        event.getType(),
+                        "old view: " + shortViewInfo(event.getOldView())
+                                + ", new view: "
+                                + shortViewInfo(event.getNewView()) + ". "
+                                + details);
+            }
+        }
+        addDiscoveryLiteHistoryEntry();
+    }
+
+    /**
+     * find a particular instance in the topology
+     */
+    private InstanceDescription findInstance(final TopologyView view,
+            final String slingId) {
+        Set<InstanceDescription> foundInstances = view
+                .findInstances(new InstanceFilter() {
+
+                    public boolean accept(InstanceDescription instance) {
+                        return instance.getSlingId().equals(slingId);
+                    }
+                });
+        if (foundInstances.size() == 1) {
+            return foundInstances.iterator().next();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * add a log entry and truncate the log entries if necessary
+     */
+    private synchronized void addEventLog(final Type type, final String info) {
+        final String logEntry = getCurrentDateFormatted() + ": " + type + ". " + info;
+
+        if (type == Type.PROPERTIES_CHANGED) {
+            propertyChangeLog.add(logEntry);
+            while (propertyChangeLog.size() > 12) {
+                propertyChangeLog.remove(0);
+            }
+        } else {
+            topologyLog.add(logEntry);
+            while (topologyLog.size() > 12) {
+                topologyLog.remove(0);
+            }
+        }
+
+    }
+
+    /**
+     * add a discoveryLite descriptor entry to the history, truncating if necessary
+     */
+    private synchronized void addDiscoveryLiteHistoryEntry() {
+        ResourceResolver resourceResolver = null;
+        try{
+            resourceResolver = getResourceResolver();
+            DiscoveryLiteDescriptor descriptor = 
+                    DiscoveryLiteDescriptor.getDescriptorFrom(resourceResolver);
+
+            final String logEntry = getCurrentDateFormatted() + ": " + descriptor.getDescriptorStr();
+            
+            discoveryLiteHistory.add(logEntry);
+            while (discoveryLiteHistory.size() > 12) {
+                discoveryLiteHistory.remove(0);
+            }
+        } catch(Exception e) {
+            logger.error("addDiscoveryLiteHistoryEntry: Exception: "+e, e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+        
+    }
+
+    /**
+     * compile a short information string of the topology, including
+     * number of clusters and instances
+     */
+    private String shortViewInfo(final TopologyView view) {
+        int clusters = view.getClusterViews().size();
+        int instances = view.getInstances().size();
+        return ((clusters == 1) ? "1 cluster" : clusters + " clusters") + ", "
+                + ((instances == 1) ? "1 instance" : instances + " instances");
+    }
+
+    /**
+     * calculate the difference between two sets of properties
+     */
+    private StringBuilder diff(final Map<String, String> oldProps,
+            final Map<String, String> newProps) {
+        final Set<String> oldKeys = new HashSet<String>(oldProps.keySet());
+        final Set<String> newKeys = new HashSet<String>(newProps.keySet());
+
+        StringBuilder sb = new StringBuilder();
+
+        for (Iterator<String> it = oldKeys.iterator(); it.hasNext();) {
+            String oldKey = it.next();
+            if (newKeys.contains(oldKey)) {
+                if (oldProps.get(oldKey).equals(newProps.get(oldKey))) {
+                    // perfect
+                } else {
+                    sb.append("(" + oldKey + " changed from "
+                            + oldProps.get(oldKey) + " to "
+                            + newProps.get(oldKey) + ")");
+                }
+                newKeys.remove(oldKey);
+            } else {
+                sb.append("(" + oldKey + " was removed)");
+            }
+            it.remove();
+        }
+        for (Iterator<String> it = newKeys.iterator(); it.hasNext();) {
+            String newKey = it.next();
+            sb.append("(" + newKey + " was added)");
+        }
+
+        return sb;
+    }
+
+    public void printConfiguration( final PrintWriter pw ) {
+        final TopologyView topology = this.currentView;
+
+        pw.println(TITLE);
+        pw.println("---------------------------------------");
+        pw.println();
+        if ( topology == null ) {
+            pw.println("No topology available yet!");
+            return;
+        }
+        pw.print("Topology");
+        if (!topology.isCurrent()) {
+            pw.print(" CHANGING! (the view is no longer current!)");
+        }
+        pw.println();
+        pw.println();
+
+        final Set<ClusterView> clusters = topology.getClusterViews();
+        final ClusterView myCluster = topology.getLocalInstance().getClusterView();
+        printCluster(pw, myCluster, myCluster);
+
+        for (Iterator<ClusterView> it = clusters.iterator(); it.hasNext();) {
+            ClusterView clusterView = it.next();
+            if (clusterView.equals(myCluster)) {
+                // skip - I already rendered that
+                continue;
+            }
+            printCluster(pw, clusterView, myCluster);
+        }
+
+        pw.println();
+        pw.println();
+
+        final Collection<CachedAnnouncement> incomingConnections = announcementRegistry.listLocalIncomingAnnouncements();
+        if ( incomingConnections.size() > 0 ) {
+            pw.println("Incoming topology connectors");
+            pw.println("---------------------------------------");
+
+            for(final CachedAnnouncement incomingCachedAnnouncement : incomingConnections) {
+                Announcement incomingAnnouncement = incomingCachedAnnouncement.getAnnouncement();
+                pw.print("Owner Sling Id : ");
+                pw.print(incomingAnnouncement.getOwnerId());
+                pw.println();
+                if (incomingAnnouncement.getServerInfo() != null) {
+                    pw.print("Server Info : ");
+                    pw.print(incomingAnnouncement.getServerInfo());
+                    pw.println();
+                }
+                pw.println("Last heartbeat received : "+beautifiedTimeDiff(incomingCachedAnnouncement.getLastPing()));
+                pw.println("Timeout : "+beautifiedDueTime(incomingCachedAnnouncement.getSecondsUntilTimeout()));
+
+                pw.println();
+            }
+            pw.println();
+            pw.println();
+        }
+
+        final Collection<TopologyConnectorClientInformation> outgoingConnections = connectorRegistry.listOutgoingConnectors();
+        if ( outgoingConnections.size() > 0 ) {
+            pw.println("Outgoing topology connectors");
+            pw.println("---------------------------------------");
+
+            for(final TopologyConnectorClientInformation topologyConnectorClient : outgoingConnections) {
+                final String remoteSlingId = topologyConnectorClient.getRemoteSlingId();
+                final boolean autoStopped = topologyConnectorClient.isAutoStopped();
+                final boolean isConnected = topologyConnectorClient.isConnected() && remoteSlingId != null;
+                pw.print("Connector URL : ");
+                pw.print(topologyConnectorClient.getConnectorUrl());
+                pw.println();
+
+                if (autoStopped) {
+                    pw.println("Conncted to Sling Id : auto-stopped");
+                    pw.println("Connector status : auto-stopped due to local-loop");
+                } else if (isConnected && !topologyConnectorClient.representsLoop()) {
+                    pw.print("Connected to Sling Id : ");
+                    pw.println(remoteSlingId);
+                    pw.println("Connector status : ok, in use");
+                } else if (topologyConnectorClient.representsLoop()) {
+                    pw.print("Connected to Sling Id : ");
+                    pw.println(remoteSlingId);
+                    pw.println("Connector status : ok, unused (loop or duplicate): standby");
+                } else {
+                    final int statusCode = topologyConnectorClient.getStatusCode();
+                    final String statusDetails = topologyConnectorClient.getStatusDetails();
+                    final String tooltipText;
+                    switch(statusCode) {
+                        case HttpServletResponse.SC_UNAUTHORIZED:
+                            tooltipText = HttpServletResponse.SC_UNAUTHORIZED +
+                                ": possible setup issue of discovery.oak on target instance, or wrong URL";
+                            break;
+                        case HttpServletResponse.SC_NOT_FOUND:
+                            tooltipText = HttpServletResponse.SC_NOT_FOUND +
+                                ": possible white list rejection by target instance";
+                            break;
+                        case -1:
+                            tooltipText = "-1: check error log. possible connection refused.";
+                            break;
+                        default:
+                            tooltipText = null;
+                    }
+                    pw.println("Connected to Sling Id : not connected");
+                    pw.print("Connector status : not ok");
+                    if ( tooltipText != null ) {
+                        pw.print(" (");
+                        pw.print(tooltipText);
+                        pw.print(")");
+                    }
+                    pw.print(" (HTTP StatusCode: "+statusCode+", "+statusDetails+")");
+                    pw.println();
+                    pw.println("Last heartbeat sent : "+beautifiedTimeDiff(topologyConnectorClient.getLastPingSent()));
+                    pw.println("Next heartbeat due : "+beautifiedDueTime(topologyConnectorClient.getNextPingDue()));
+                }
+                pw.println();
+            }
+            pw.println();
+            pw.println();
+        }
+
+        ResourceResolver resourceResolver = null;
+        pw.println("Discovery-Lite Descriptor History");
+        pw.println("---------------------------------------");
+        for (String discoLiteHistoryEntry : discoveryLiteHistory) {
+            pw.println(discoLiteHistoryEntry);
+        }
+        pw.println();
+        pw.println();
+        pw.println("Current Discovery-Lite Descriptor Value");
+        pw.println("---------------------------------------");
+        try{
+            resourceResolver = getResourceResolver();
+            DiscoveryLiteDescriptor descriptor = DiscoveryLiteDescriptor.getDescriptorFrom(resourceResolver);
+            final String logEntry = getCurrentDateFormatted() + ": " + descriptor.getDescriptorStr();
+            pw.println(logEntry);
+            pw.println();
+            pw.println();
+        } catch(Exception e) {
+            logger.error("renderOverview: Exception: "+e, e);
+            pw.println("Got exception trying to get repository descriptor: "+e);
+            pw.println();
+            pw.println();
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+
+        if ( topologyLog.size() > 0 ) {
+            pw.println("Topology Change History");
+            pw.println("---------------------------------------");
+            for(final String aLogEntry : topologyLog) {
+                pw.println(aLogEntry);
+            }
+            pw.println();
+            pw.println();
+        }
+
+        if ( propertyChangeLog.size() > 0 ) {
+            pw.println("Property Change History");
+            pw.println("---------------------------------------");
+            for(final String aLogEntry : propertyChangeLog) {
+                pw.println(aLogEntry);
+            }
+            pw.println();
+        }
+    }
+
+    private String getCurrentDateFormatted() {
+        return sdf.format(Calendar.getInstance().getTime());
+    }
+
+    /**
+     * Render a particular cluster
+     */
+    private void printCluster(final PrintWriter pw, final ClusterView renderCluster, final ClusterView localCluster) {
+        final Collection<Announcement> announcements = announcementRegistry.listAnnouncementsInSameCluster(localCluster);
+
+        for(final InstanceDescription instanceDescription : renderCluster.getInstances() ) {
+            final boolean inLocalCluster = renderCluster == localCluster;
+            Announcement parentAnnouncement = null;
+            for (Iterator<Announcement> it2 = announcements.iterator(); it2
+                    .hasNext();) {
+                Announcement announcement = it2.next();
+                for (Iterator<InstanceDescription> it3 = announcement
+                        .listInstances().iterator(); it3.hasNext();) {
+                    InstanceDescription announcedInstance = it3.next();
+                    if (announcedInstance.getSlingId().equals(
+                            instanceDescription.getSlingId())) {
+                        parentAnnouncement = announcement;
+                        break;
+                    }
+                }
+            }
+
+            final boolean isLocal = instanceDescription.isLocal();
+            final String slingId = instanceDescription.getSlingId();
+
+            pw.print("Sling ID : ");
+            pw.print(slingId);
+            pw.println();
+            pw.print("Cluster View ID : ");
+            pw.print(instanceDescription.getClusterView() == null ? "null"
+                            : instanceDescription.getClusterView().getId());
+            pw.println();
+            pw.print("Local instance : ");
+            pw.print(isLocal);
+            pw.println();
+            pw.print("Leader instance : ");
+            pw.print(instanceDescription.isLeader());
+            pw.println();
+            pw.print("In local cluster : ");
+            if (inLocalCluster) {
+                pw.print("local");
+            } else {
+                pw.print("remote");
+            }
+            pw.println();
+            pw.print("Announced by : ");
+            if (inLocalCluster) {
+                pw.print("n/a");
+            } else {
+                if (parentAnnouncement != null) {
+                    pw.print(parentAnnouncement.getOwnerId());
+                } else {
+                    pw.print("(changing)");
+                }
+            }
+            pw.println();
+
+            pw.println("Properties:");
+            for(final Map.Entry<String, String> entry : instanceDescription.getProperties().entrySet()) {
+                pw.print("- ");
+                pw.print(entry.getKey());
+                pw.print(" : ");
+                pw.print(entry.getValue());
+                pw.println();
+            }
+            pw.println();
+            pw.println();
+        }
+    }
+}

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/TopologyWebConsolePlugin.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/cluster/OakClusterViewService.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/cluster/OakClusterViewService.java?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/cluster/OakClusterViewService.java (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/cluster/OakClusterViewService.java Wed Oct 21 15:50:37 2015
@@ -0,0 +1,227 @@
+/*
+ * 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.oak.cluster;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException.Reason;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.apache.sling.discovery.commons.providers.spi.base.DiscoveryLiteDescriptor;
+import org.apache.sling.discovery.commons.providers.spi.base.IdMapService;
+import org.apache.sling.discovery.oak.Config;
+import org.apache.sling.settings.SlingSettingsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Oak-based implementation of the ClusterViewService interface.
+ */
+@Component
+@Service(value = ClusterViewService.class)
+public class OakClusterViewService implements ClusterViewService {
+    
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private SlingSettingsService settingsService;
+
+    @Reference
+    private ResourceResolverFactory resourceResolverFactory;
+
+    @Reference
+    private Config config;
+    
+    @Reference
+    private IdMapService idMapService;
+    
+    public static OakClusterViewService testConstructor(SlingSettingsService settingsService,
+            ResourceResolverFactory resourceResolverFactory,
+            IdMapService idMapService,
+            Config config) {
+        OakClusterViewService service = new OakClusterViewService();
+        service.settingsService = settingsService;
+        service.resourceResolverFactory = resourceResolverFactory;
+        service.config = config;
+        service.idMapService = idMapService;
+        return service;
+    }
+    
+    public String getSlingId() {
+    	if (settingsService==null) {
+    		return null;
+    	}
+        return settingsService.getSlingId();
+    }
+
+    protected ResourceResolver getResourceResolver() throws LoginException {
+        return resourceResolverFactory.getAdministrativeResourceResolver(null);
+    }
+
+    public LocalClusterView getLocalClusterView() throws UndefinedClusterViewException {
+        logger.trace("getLocalClusterView: start");
+        ResourceResolver resourceResolver = null;
+        try{
+            resourceResolver = getResourceResolver();
+            DiscoveryLiteDescriptor descriptor = 
+                    DiscoveryLiteDescriptor.getDescriptorFrom(resourceResolver);
+            return asClusterView(descriptor, resourceResolver);
+        } catch (UndefinedClusterViewException e) {
+            logger.info("getLocalClusterView: undefined clusterView: "+e.getReason()+" - "+e.getMessage());
+            throw e;
+        } catch (Exception e) {
+            logger.error("getLocalClusterView: repository exception: "+e, e);
+            throw new UndefinedClusterViewException(Reason.REPOSITORY_EXCEPTION, "Exception while processing descriptor: "+e);
+        } finally {
+            logger.trace("getLocalClusterView: end");
+            if (resourceResolver!=null) {
+                resourceResolver.close();
+            }
+        }
+    }
+
+    private LocalClusterView asClusterView(DiscoveryLiteDescriptor descriptor, ResourceResolver resourceResolver) throws Exception {
+        if (descriptor == null) {
+            throw new IllegalArgumentException("descriptor must not be null");
+        }
+        if (resourceResolver==null) {
+            throw new IllegalArgumentException("resourceResolver must not be null");
+        }
+        logger.trace("asClusterView: start");
+        String clusterViewId = descriptor.getViewId();
+        String localClusterSyncTokenId = descriptor.getViewId()+"_"+descriptor.getSeqNum();
+        if (!descriptor.isFinal()) {
+            throw new UndefinedClusterViewException(Reason.NO_ESTABLISHED_VIEW, "descriptor is not yet final: "+descriptor);
+        }
+        LocalClusterView cluster = new LocalClusterView(clusterViewId, localClusterSyncTokenId);
+        long me = descriptor.getMyId();
+        int[] activeIds = descriptor.getActiveIds();
+        if (activeIds==null || activeIds.length==0) {
+            throw new UndefinedClusterViewException(Reason.NO_ESTABLISHED_VIEW, "Descriptor contained no active ids: "+descriptor.getDescriptorStr());
+        }
+        // convert int[] to List<Integer>
+        //TODO: could use Guava's Ints class here..
+        List<Integer> activeIdsList = new LinkedList<Integer>();
+        for (Integer integer : activeIds) {
+            activeIdsList.add(integer);
+        }
+        
+        // step 1: sort activeIds by their leaderElectionId
+        //   serves two purposes: pos[0] is then leader
+        //   and the rest are properly sorted within the cluster
+        final Map<Integer, String> leaderElectionIds = new HashMap<Integer, String>();
+        for (Integer id : activeIdsList) {
+            String slingId = idMapService.toSlingId(id, resourceResolver);
+            if (slingId == null) {
+                throw new UndefinedClusterViewException(Reason.NO_ESTABLISHED_VIEW,
+                        "no slingId mapped for clusterNodeId="+id);
+            }
+            String leaderElectionId = getLeaderElectionId(resourceResolver,
+                    slingId);
+            leaderElectionIds.put(id, leaderElectionId);
+        }
+        
+        Collections.sort(activeIdsList, new Comparator<Integer>() {
+
+            @Override
+            public int compare(Integer arg0, Integer arg1) {
+                return leaderElectionIds.get(arg0)
+                        .compareTo(leaderElectionIds.get(arg1));
+            }
+        });
+        
+        for(int i=0; i<activeIdsList.size(); i++) {
+            int id = activeIdsList.get(i);
+            boolean isLeader = i==0; // thx to sorting above [0] is leader indeed
+            boolean isOwn = id==me;
+            String slingId = idMapService.toSlingId(id, resourceResolver);
+            if (slingId==null) {
+                logger.info("asClusterView: cannot resolve oak-clusterNodeId {} to a slingId", id);
+                throw new Exception("Cannot resolve oak-clusterNodeId "+id+" to a slingId");
+            }
+            Map<String, String> properties = readProperties(slingId, resourceResolver);
+            // create a new instance (adds itself to the cluster in the constructor)
+            new DefaultInstanceDescription(cluster, isLeader, isOwn, slingId, properties);
+        }
+        logger.trace("asClusterView: returning {}", cluster);
+        InstanceDescription local = cluster.getLocalInstance();
+        if (local != null) {
+            return cluster;
+        } else {
+            logger.info("getClusterView: the local instance ("+getSlingId()+") is currently not included in the existing established view! "
+                    + "This is normal at startup. At other times is pseudo-network-partitioning is an indicator for repository/network-delays or clocks-out-of-sync (SLING-3432). "
+                    + "(increasing the heartbeatTimeout can help as a workaround too) "
+                    + "The local instance will stay in TOPOLOGY_CHANGING or pre _INIT mode until a new vote was successful.");
+            throw new UndefinedClusterViewException(Reason.ISOLATED_FROM_TOPOLOGY, 
+                    "established view does not include local instance - isolated");
+        }
+    }
+
+    private String getLeaderElectionId(ResourceResolver resourceResolver, String slingId) {
+        if (slingId==null) {
+            throw new IllegalStateException("slingId must not be null");
+        }
+        final String myClusterNodePath = config.getClusterInstancesPath()+"/"+slingId;
+        ValueMap resourceMap = resourceResolver.getResource(myClusterNodePath)
+                .adaptTo(ValueMap.class);
+        String result = resourceMap.get("leaderElectionId", String.class);
+        return result;
+    }
+    
+    private Map<String, String> readProperties(String slingId, ResourceResolver resourceResolver) {
+        Resource res = resourceResolver.getResource(
+                        config.getClusterInstancesPath() + "/"
+                                + slingId);
+        final Map<String, String> props = new HashMap<String, String>();
+        if (res != null) {
+            final Resource propertiesChild = res.getChild("properties");
+            if (propertiesChild != null) {
+                final ValueMap properties = propertiesChild.adaptTo(ValueMap.class);
+                if (properties != null) {
+                    for (Iterator<String> it = properties.keySet().iterator(); it
+                            .hasNext();) {
+                        String key = it.next();
+                        if (!key.equals("jcr:primaryType")) {
+                            props.put(key, properties.get(key, String.class));
+                        }
+                    }
+                }
+            }
+        }
+        return props;
+    }
+
+}

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/cluster/OakClusterViewService.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/pinger/OakViewChecker.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/pinger/OakViewChecker.java?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/pinger/OakViewChecker.java (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/pinger/OakViewChecker.java Wed Oct 21 15:50:37 2015
@@ -0,0 +1,322 @@
+/*
+ * 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.oak.pinger;
+
+import java.util.Calendar;
+import java.util.UUID;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.commons.BaseViewChecker;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.commons.providers.util.ResourceHelper;
+import org.apache.sling.discovery.oak.Config;
+import org.apache.sling.discovery.oak.OakDiscoveryService;
+import org.apache.sling.launchpad.api.StartupListener;
+import org.apache.sling.launchpad.api.StartupMode;
+import org.apache.sling.settings.SlingSettingsService;
+import org.osgi.service.http.HttpService;
+
+/**
+ * The OakViewChecker is taking care of checking the oak discovery-lite
+ * descriptor when checking the local cluster view and passing that
+ * on to the ViewStateManager which will then detect whether there was 
+ * any change or not. Unlike discovery.impl's HeartbeatHandler this one
+ * does not store any heartbeats in the repository anymore.
+ * <p>
+ * Remote heartbeats are POSTs to remote TopologyConnectorServlets using
+ * discovery.base
+ */
+@Component
+@Service(value = { OakViewChecker.class, StartupListener.class })
+@Reference(referenceInterface=HttpService.class,
+           cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
+           policy=ReferencePolicy.DYNAMIC)
+public class OakViewChecker extends BaseViewChecker {
+
+    @Reference
+    protected SlingSettingsService slingSettingsService;
+
+    @Reference
+    protected ResourceResolverFactory resourceResolverFactory;
+
+    @Reference
+    protected ConnectorRegistry connectorRegistry;
+
+    @Reference
+    protected AnnouncementRegistry announcementRegistry;
+
+    @Reference
+    protected Scheduler scheduler;
+
+    @Reference
+    private Config config;
+
+    private OakDiscoveryService discoveryService;
+
+    /** for testing only **/
+    public static OakViewChecker testConstructor(
+            SlingSettingsService slingSettingsService,
+            ResourceResolverFactory resourceResolverFactory,
+            ConnectorRegistry connectorRegistry,
+            AnnouncementRegistry announcementRegistry,
+            Scheduler scheduler,
+            Config config) {
+        OakViewChecker pinger = new OakViewChecker();
+        pinger.slingSettingsService = slingSettingsService;
+        pinger.resourceResolverFactory = resourceResolverFactory;
+        pinger.connectorRegistry = connectorRegistry;
+        pinger.announcementRegistry = announcementRegistry;
+        pinger.scheduler = scheduler;
+        pinger.config = config;
+        return pinger;
+    }
+
+    @Override
+    protected AnnouncementRegistry getAnnouncementRegistry() {
+        return announcementRegistry;
+    }
+    
+    @Override
+    protected BaseConfig getConnectorConfig() {
+        return config;
+    }
+    
+    @Override
+    protected ConnectorRegistry getConnectorRegistry() {
+        return connectorRegistry;
+    }
+    
+    @Override
+    protected ResourceResolverFactory getResourceResolverFactory() {
+        return resourceResolverFactory;
+    }
+    
+    @Override
+    protected Scheduler getScheduler() {
+        return scheduler;
+    }
+    
+    @Override
+    protected SlingSettingsService getSlingSettingsService() {
+        return slingSettingsService;
+    }
+    
+    @Override
+    protected void doActivate() {
+        // on activate the resetLeaderElectionId is set to true to ensure that
+        // the 'leaderElectionId' property is reset on next heartbeat issuance.
+        // the idea being that a node which leaves the cluster should not
+        // become leader on next join - and by resetting the leaderElectionId
+        // to the current time, this is ensured.
+        runtimeId = UUID.randomUUID().toString();
+
+        logger.info("doActivate: activated with runtimeId: {}, slingId: {}", runtimeId, slingId);
+        
+        resetLeaderElectionId();
+    }
+    
+    @Override
+    public void startupFinished(StartupMode mode) {
+        super.startupFinished(mode);
+        
+        synchronized(lock) {
+            if (activated) {
+                // only reset if activated
+                resetLeaderElectionId();
+            }
+        }
+
+    }
+    
+    /**
+     * The initialize method is called by the OakDiscoveryService.activate
+     * as we require the discoveryService (and the discoveryService has
+     * a reference on us - but we cant have circular references in osgi).
+     */
+    public void initialize(final OakDiscoveryService discoveryService) {
+        logger.info("initialize: initializing.");
+        synchronized(lock) {
+        	this.discoveryService = discoveryService;
+            issueHeartbeat();
+        }
+
+        // start the (less frequent) periodic job that does the
+        // connector pings and checks the connector/topology view
+        try {
+            final long interval = config.getConnectorPingInterval();
+            logger.info("initialize: starting periodic connectorPing job for "+slingId+" with interval "+interval+" sec.");
+            scheduler.addPeriodicJob(NAME+".connectorPinger", this,
+                    null, interval, false);
+        } catch (Exception e) {
+            logger.error("activate: Could not start heartbeat runner: " + e, e);
+        }
+        
+        // start the (more frequent) periodic job that checks
+        // the discoveryLite descriptor - that can be more frequent
+        // since it is only reading an oak repository descriptor
+        // which is designed to be read very frequently (it caches
+        // the value and only updates it on change, so reading is very cheap)
+        // and because doing this more frequently means that the
+        // reaction time is faster
+        try{
+            final long interval = config.getDiscoveryLiteCheckInterval();
+            logger.info("initialize: starting periodic discoveryLiteCheck job for "+slingId+" with interval "+interval+" sec.");
+            scheduler.addPeriodicJob(NAME+".discoveryLiteCheck", new Runnable() {
+
+                @Override
+                public void run() {
+                    discoveryLiteCheck();
+                }
+                
+            },
+                    null, interval, false);
+        } catch (Exception e) {
+            logger.error("activate: Could not start heartbeat runner: " + e, e);
+        }
+    }
+    
+    private void discoveryLiteCheck() {
+        logger.debug("discoveryLiteCheck: start. [for slingId="+slingId+"]");
+        synchronized(lock) {
+            if (!activated) {
+                // SLING:2895: avoid checks if not activated
+                logger.debug("discoveryLiteCheck: not activated yet");
+                return;
+            }
+
+            // check the view
+            // discovery.oak relies on oak's discovery-lite descriptor
+            // to be updated independently in case of cluster view change.
+            // all that we can therefore do here is assume something
+            // might have changed and let discoveryService/viewStateManager
+            // filter out the 99.99% of unchanged cases.
+            discoveryService.handlePotentialTopologyChange();
+        }
+        logger.debug("discoveryLiteCheck: end. [for slingId="+slingId+"]");
+    }
+
+    /** Get or create a ResourceResolver **/
+    private ResourceResolver getResourceResolver() throws LoginException {
+        if (resourceResolverFactory == null) {
+            logger.error("getResourceResolver: resourceResolverFactory is null!");
+            return null;
+        }
+        return resourceResolverFactory.getAdministrativeResourceResolver(null);
+    }
+
+    /** Calcualte the local cluster instance path **/
+    private String getLocalClusterNodePath() {
+        return config.getClusterInstancesPath() + "/" + slingId;
+    }
+
+    /**
+     * Hook that will cause a reset of the leaderElectionId 
+     * on next invocation of issueClusterLocalHeartbeat.
+     * @return true if the leaderElectionId was reset - false if that was not
+     * necessary as that happened earlier already and it has not propagated
+     * yet to the ./clusterInstances in the meantime
+     */
+    public boolean resetLeaderElectionId() {
+        ResourceResolver resourceResolver = null;
+        try{
+            final String myClusterNodePath = getLocalClusterNodePath();
+            resourceResolver = getResourceResolver();
+            if (resourceResolver==null) {
+                logger.warn("resetLeaderElectionId: could not login, new leaderElectionId will be calculated upon next heartbeat only!");
+                return false;
+            }
+            String newLeaderElectionId = newLeaderElectionId();
+
+            final Resource resource = ResourceHelper.getOrCreateResource(
+                    resourceResolver, myClusterNodePath);
+            final ModifiableValueMap resourceMap = resource.adaptTo(ModifiableValueMap.class);
+
+            resourceMap.put(PROPERTY_ID_RUNTIME, runtimeId);
+            // SLING-4765 : store more infos to be able to be more verbose on duplicate slingId/ghost detection
+            final String slingHomePath = slingSettingsService==null ? "n/a" : slingSettingsService.getSlingHomePath();
+            resourceMap.put(PROPERTY_ID_SLING_HOME_PATH, slingHomePath);
+            final String endpointsAsString = getEndpointsAsString();
+            resourceMap.put(PROPERTY_ID_ENDPOINTS, endpointsAsString);
+
+            Calendar leaderElectionCreatedAt = Calendar.getInstance();
+            resourceMap.put("leaderElectionId", newLeaderElectionId);
+            resourceMap.put("leaderElectionIdCreatedAt", leaderElectionCreatedAt);
+
+            logger.info("resetLeaderElectionId: storing my runtimeId: {}, endpoints: {} and sling home path: {}", 
+                    new Object[]{runtimeId, endpointsAsString, slingHomePath, newLeaderElectionId, leaderElectionCreatedAt});
+            resourceResolver.commit();
+        } catch (LoginException e) {
+            logger.error("resetLeaderElectionid: could not login: "+e, e);
+        } catch (PersistenceException e) {
+            logger.error("resetLeaderElectionid: got PersistenceException: "+e, e);
+        } finally {
+            if (resourceResolver!=null) {
+                resourceResolver.close();
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Calculate a new leaderElectionId based on the current config and system time
+     */
+    private String newLeaderElectionId() {
+        int maxLongLength = String.valueOf(Long.MAX_VALUE).length();
+        String currentTimeMillisStr = String.format("%0"
+                + maxLongLength + "d", System.currentTimeMillis());
+
+        String prefix = "1";
+
+        final String newLeaderElectionId = prefix + "_"
+                + currentTimeMillisStr + "_" + slingId;
+        return newLeaderElectionId;
+    }
+
+    @Override
+    protected void doCheckView() {
+        super.doCheckView();
+
+        // discovery.oak relies on oak's discovery-lite descriptor
+        // to be updated independently in case of cluster view change.
+        // all that we can therefore do here is assume something
+        // might have changed and let discoveryService/viewStateManager
+        // filter out the 99.99% of unchanged cases.
+        discoveryService.handlePotentialTopologyChange();
+    }
+
+    protected void updateProperties() {
+        if (discoveryService == null) {
+            logger.error("issueHeartbeat: discoveryService is null");
+        } else {
+            discoveryService.updateProperties();
+        }
+    }}

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/main/java/org/apache/sling/discovery/oak/pinger/OakViewChecker.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: sling/trunk/bundles/extensions/discovery/oak/src/main/resources/OSGI-INF/metatype/metatype.properties
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/main/resources/OSGI-INF/metatype/metatype.properties?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/main/resources/OSGI-INF/metatype/metatype.properties (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/main/resources/OSGI-INF/metatype/metatype.properties Wed Oct 21 15:50:37 2015
@@ -0,0 +1,121 @@
+#
+#  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.
+#
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the SCR plugin
+config.name=Apache Sling Oak-Based Discovery Service Configuration
+config.description = The configuration of the Oak based discovery service implementation.
+
+connectorPingTimeout.name = Connector Ping timeout (seconds)
+connectorPingTimeout.description = Configure the timeout (in seconds) after which an announcement \
+ sent via a topology connector is considered timed out. Default is 120 seconds.
+
+connectorPingInterval.name = Connector Ping interval (seconds)
+connectorPingInterval.description = Configure the interval (in seconds) according to which the \
+ topology connector pings are exchanged in the topology. Default is 30 seconds.
+
+discoveryLiteCheckInterval.name = Discovery-Lite Check interval (seconds)
+discoveryLiteCheckInterval.description = Configure the interval (in seconds) with which Oak's \
+ discoveryLite descriptor should be checked for changes. Default is 2 seconds. \
+ Note that the timeout value is configured within Oak directly.
+
+minEventDelay.name = Minimal Event Delay (seconds)
+minEventDelay.description = Configure a minimal delay (in seconds) between TOPOLOGY_CHANGING \
+ and TOPOLOGY_CHANGED. Any further changes happening during this delay are accumulated and \
+ combined in the TOPOLOGY_CHANGED after this delay. This helps avoiding event-flooding. \
+ Default is 3 seconds. A negative value or zero disables this delay.
+
+topologyConnectorUrls.name = Topology Connector URLs
+topologyConnectorUrls.description = URLs where to join a topology, e.g. \
+ http://localhost:4502/libs/sling/topology/connector
+
+topologyConnectorWhitelist.name = Topology Connector Whitelist
+topologyConnectorWhitelist.description = List of IPs and/or hostnames which are allowed to \
+ connect to the connector URL. There are four variants here: 1. provide a plain hostname. \
+ 2. provide an IP address. 3. provide a hostname or IP address with wildcards (* or ?). \
+ 4. provide an IP address with a subnet mask, either using the CIDR notation: 1.2.3.4/24 \
+ or an IP address, space, subnet mask: 1.2.3.4 255.255.255.0)
+
+discoveryResourcePath.name = Discovery Resource Path
+discoveryResourcePath.description = Path of resource where to keep discovery information. \
+ The default is /var/discovery/oak.
+
+leaderElectionRepositoryDescriptor.name = Repository Descriptor Name
+leaderElectionRepositoryDescriptor.description = Name of the repository descriptor to be taken \
+ into account for leader election: those instances have preference to become leader which have \
+ the corresponding descriptor value of 'false'.
+ 
+invertRepositoryDescriptor.name = Invert Repository Descriptor
+invertRepositoryDescriptor.description = Enabling this property allows to invert the \
+ repository descriptor value that is obtained via the configured 'leaderElectionRepositoryDescriptor' \
+ (thus only applies if that is configured). Default is 'false' (don't invert).
+
+autoStopLocalLoopEnabled.name = Auto-Stop Local-Loops
+autoStopLocalLoopEnabled.description = If true, and the discovery.impl detects a local-looping \
+ topology connector, the corresponding topology connector will be automatically stopped. \
+ This is useful to prevent unnecessary loops with eg pre-configured topology connectors.
+
+gzipConnectorRequestsEnabled.name = gzip requests
+gzipConnectorRequestsEnabled.description = If true, the payloads of topology connector requests \
+ will be gzipped. This is advisable on certain connector structures, eg in a tree structure, where \
+ a topology connector announces a large sub-topology. Note that this only works with \
+ the server running discovery.impl 1.0.4 and onwards. Replies are gzipped automatically.
+
+socketConnectTimeout.name = connector's socket.connect() timeout
+socketConnectTimeout.description = Timeout (in seconds!) for the topology connector's \
+ socket.connect()
+ 
+soTimeout.name = connector's read timeout
+soTimeout.description = Topology connector's socket timeout (SO_TIMEOUT) (in seconds!) which is \
+ the timeout for waiting for data
+
+hmacEnabled.name = Enable Hmac message signatures
+hmacEnabled.description = If true, and the Shared Key is set to the same value on all members of the \
+ topology, the messages will be validated using a HMAC of a digest of the body of the message. \
+ The hmac and message digest are in the HTTP request and response headers. Both requests and responses \
+ are signed.
+
+enableEncryption.name = Enable Message encryption
+enableEncryption.description = If Message HMACs are enabled and there is a shared key set, setting this to \
+ true will encrypt the body of the message using 128 bit AES encryption. Once encrypted you will not be able \
+ debug the messages at the http level.
+
+sharedKey.name = Message shared key.
+sharedKey.description = If message signing and encryption is used, this should be set to the same value \
+ on all members of the same topology. If any member of the topology has a different key it will effectively \
+ be excluded from the topology even if it attempts to send messages to other members of the topology.
+
+hmacSharedKeyTTL.name = Shared Key TTL
+hmacSharedKeyTTL.description = Shared keys for message signatures are derived from the configured shared key. \
+ Each derived key has a lifetime (TTL). Once that time has expired a new key is derived and used for hmac signatures. \
+ This setting, sets the TTL in ms. Keys that are 2 lifetimes old are ignored. Set according to you level of paranoia, \
+ but don't set to less than the greatest possible clock drift between members of the topology. The default is 4 hours. Setting \
+ to a ridiculously low value will increase the turnover of keys. Generating a key takes about 2ms. There is no risk of \
+ memory consumption with low values, only a risk of the topology falling apart due to incorrectly set clocks.
+
+backoffStableFactor.name = Backoff factor for stable connectors
+backoffStableFactor.description = When a topology connector is stable (ie no changes occuring in the announcements sent), \
+ then the heartbeat frequency is lowered, ie the heartbeatInterval for this connector is steadily increased, at maximum by the \
+ backoffStableFactor.
+
+backoffStandbyFactor.name = Backoff factor for standby connectors
+backoffStandbyFactor.description = When a topology connector is in standby mode (ie when it is redundant), the heartbeat \
+ frequency is lowered, ie the heartbeatInterval for this connector is increased , at maximum by the backoffStandbyFactor

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/main/resources/OSGI-INF/metatype/metatype.properties
------------------------------------------------------------------------------
    svn:eol-style = native

Added: sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/OakDiscoveryServiceTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/OakDiscoveryServiceTest.java?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/OakDiscoveryServiceTest.java (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/OakDiscoveryServiceTest.java Wed Oct 21 15:50:37 2015
@@ -0,0 +1,116 @@
+/*
+ * 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.oak;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.UUID;
+
+import org.apache.sling.discovery.commons.providers.base.DummyListener;
+import org.apache.sling.discovery.commons.providers.spi.base.DescriptorHelper;
+import org.apache.sling.discovery.commons.providers.spi.base.DiscoveryLiteConfig;
+import org.apache.sling.discovery.commons.providers.spi.base.DiscoveryLiteDescriptorBuilder;
+import org.apache.sling.discovery.commons.providers.spi.base.DummySlingSettingsService;
+import org.apache.sling.discovery.commons.providers.spi.base.IdMapService;
+import org.apache.sling.discovery.oak.its.setup.OakVirtualInstanceBuilder;
+import org.junit.Test;
+
+public class OakDiscoveryServiceTest {
+
+    public final class SimpleCommonsConfig implements DiscoveryLiteConfig {
+        
+        private long bgIntervalMillis;
+        private long bgTimeoutMillis;
+
+        SimpleCommonsConfig(long bgIntervalMillis, long bgTimeoutMillis) {
+            this.bgIntervalMillis = bgIntervalMillis;
+            this.bgTimeoutMillis = bgTimeoutMillis;
+        }
+        
+        @Override
+        public String getSyncTokenPath() {
+            return "/var/synctokens";
+        }
+
+        @Override
+        public String getIdMapPath() {
+            return "/var/idmap";
+        }
+
+        @Override
+        public long getBgTimeoutMillis() {
+            return bgTimeoutMillis;
+        }
+
+        @Override
+        public long getBgIntervalMillis() {
+            return bgIntervalMillis;
+        }
+
+    }
+
+    @Test
+    public void testBindBeforeActivate() throws Exception {
+        OakVirtualInstanceBuilder builder = 
+                (OakVirtualInstanceBuilder) new OakVirtualInstanceBuilder()
+                .setDebugName("test")
+                .newRepository("/foo/bar", true);
+        String slingId = UUID.randomUUID().toString();;
+        DiscoveryLiteDescriptorBuilder discoBuilder = new DiscoveryLiteDescriptorBuilder();
+        discoBuilder.id("id").me(1).activeIds(1);
+        // make sure the discovery-lite descriptor is marked as not final
+        // such that the view is not already set before we want it to be
+        discoBuilder.setFinal(false);
+        DescriptorHelper.setDiscoveryLiteDescriptor(builder.getResourceResolverFactory(), 
+                discoBuilder);
+        IdMapService idMapService = IdMapService.testConstructor(new SimpleCommonsConfig(1000, -1), new DummySlingSettingsService(slingId), builder.getResourceResolverFactory());
+        assertTrue(idMapService.waitForInit(2000));
+        OakDiscoveryService discoveryService = (OakDiscoveryService) builder.getDiscoverService();
+        assertNotNull(discoveryService);
+        DummyListener listener = new DummyListener();
+        for(int i=0; i<100; i++) {
+            discoveryService.bindTopologyEventListener(listener);
+            discoveryService.unbindTopologyEventListener(listener);
+        }
+        discoveryService.bindTopologyEventListener(listener);
+        assertEquals(0, listener.countEvents());
+        discoveryService.activate(null);
+        assertEquals(0, listener.countEvents());
+        // some more confusion...
+        discoveryService.unbindTopologyEventListener(listener);
+        discoveryService.bindTopologyEventListener(listener);
+        // only set the final flag now - this makes sure that handlePotentialTopologyChange
+        // will actually detect a valid new, different view and send out an event -
+        // exactly as we want to
+        discoBuilder.setFinal(true);
+        DescriptorHelper.setDiscoveryLiteDescriptor(builder.getResourceResolverFactory(), 
+                discoBuilder);
+        discoveryService.handlePotentialTopologyChange();
+        assertTrue(discoveryService.getViewStateManager().waitForAsyncEvents(2000));
+        assertEquals(1, listener.countEvents());
+        discoveryService.unbindTopologyEventListener(listener);
+        assertEquals(1, listener.countEvents());
+        discoveryService.bindTopologyEventListener(listener);
+        assertTrue(discoveryService.getViewStateManager().waitForAsyncEvents(2000));
+        assertEquals(2, listener.countEvents()); // should now have gotten an INIT too
+    }
+    
+}

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/OakDiscoveryServiceTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/its/OakClusterLoadTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/its/OakClusterLoadTest.java?rev=1709867&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/its/OakClusterLoadTest.java (added)
+++ sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/its/OakClusterLoadTest.java Wed Oct 21 15:50:37 2015
@@ -0,0 +1,32 @@
+/*
+ * 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.oak.its;
+
+import org.apache.sling.discovery.base.its.AbstractClusterLoadTest;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.oak.its.setup.OakVirtualInstanceBuilder;
+
+public class OakClusterLoadTest extends AbstractClusterLoadTest {
+
+    @Override
+    public VirtualInstanceBuilder newBuilder() {
+        return new OakVirtualInstanceBuilder();
+    }
+
+}

Propchange: sling/trunk/bundles/extensions/discovery/oak/src/test/java/org/apache/sling/discovery/oak/its/OakClusterLoadTest.java
------------------------------------------------------------------------------
    svn:eol-style = native