| /* |
| * 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.slider.server.appmaster.web.view; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Maps; |
| import com.google.inject.Inject; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.hadoop.yarn.webapp.hamlet.Hamlet; |
| import org.apache.hadoop.yarn.webapp.hamlet.Hamlet.DIV; |
| import org.apache.hadoop.yarn.webapp.hamlet.Hamlet.TABLE; |
| import org.apache.hadoop.yarn.webapp.hamlet.Hamlet.TBODY; |
| import org.apache.hadoop.yarn.webapp.hamlet.Hamlet.TR; |
| import org.apache.hadoop.yarn.webapp.view.HtmlBlock; |
| import org.apache.slider.api.ClusterDescription; |
| import org.apache.slider.api.ClusterNode; |
| import org.apache.slider.client.SliderClusterOperations; |
| import org.apache.slider.server.appmaster.state.RoleInstance; |
| import org.apache.slider.server.appmaster.state.RoleStatus; |
| import org.apache.slider.server.appmaster.web.WebAppApi; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| /** |
| * |
| */ |
| public class ContainerStatsBlock extends HtmlBlock { |
| private static final Logger log = LoggerFactory.getLogger(ContainerStatsBlock.class); |
| |
| private static final String EVEN = "even", ODD = "odd", BOLD = "bold", SCHEME = "http://", PATH = "/node/container/"; |
| |
| // Some functions that help transform the data into an object we can use to abstract presentation specifics |
| protected static final Function<Entry<String,Integer>,Entry<TableContent,Integer>> stringIntPairFunc = toTableContentFunction(); |
| protected static final Function<Entry<String,String>,Entry<TableContent,String>> stringStringPairFunc = toTableContentFunction(); |
| |
| private WebAppApi slider; |
| private SliderClusterOperations clusterOps; |
| |
| @Inject |
| public ContainerStatsBlock(WebAppApi slider) { |
| this.slider = slider; |
| clusterOps = new SliderClusterOperations(slider.getClusterProtocol()); |
| } |
| |
| /** |
| * Sort a collection of ClusterNodes by name |
| */ |
| protected static class ClusterNodeNameComparator implements Comparator<ClusterNode> { |
| |
| @Override |
| public int compare(ClusterNode node1, ClusterNode node2) { |
| if (null == node1 && null != node2) { |
| return -1; |
| } else if (null != node1 && null == node2) { |
| return 1; |
| } else if (null == node1 && null == node2) { |
| return 0; |
| } |
| |
| final String name1 = node1.name, name2 = node2.name; |
| if (null == name1 && null != name2) { |
| return -1; |
| } else if (null != name1 && null == name2) { |
| return 1; |
| } else if (null == name1 && null == name2) { |
| return 0; |
| } |
| |
| return name1.compareTo(name2); |
| } |
| |
| } |
| |
| @Override |
| protected void render(Block html) { |
| // TODO Probably better to just get a copy of this list for us to avoid the repeated synchronization? |
| // does this change if we have 50 node, 100node, 500 node clusters? |
| final Map<String,RoleInstance> containerInstances = getContainerInstances(slider.getAppState().cloneOwnedContainerList()); |
| |
| for (Entry<String,RoleStatus> entry : slider.getRoleStatusByName().entrySet()) { |
| final String name = entry.getKey(); |
| final RoleStatus roleStatus = entry.getValue(); |
| |
| DIV<Hamlet> div = html.div("role-info ui-widget-content ui-corner-all"); |
| |
| List<ClusterNode> nodesInRole; |
| try { |
| nodesInRole = clusterOps.listClusterNodesInRole(name); |
| } catch (Exception e) { |
| log.error("Could not fetch containers for role: " + name, e); |
| nodesInRole = Collections.emptyList(); |
| } |
| |
| div.h2(BOLD, StringUtils.capitalize(name)); |
| |
| // Generate the details on this role |
| Iterable<Entry<String,Integer>> stats = roleStatus.buildStatistics().entrySet(); |
| generateRoleDetails(div,"role-stats-wrap", "Specifications", Iterables.transform(stats, stringIntPairFunc)); |
| |
| // Sort the ClusterNodes by their name (containerid) |
| Collections.sort(nodesInRole, new ClusterNodeNameComparator()); |
| |
| // Generate the containers running this role |
| generateRoleDetails(div, "role-stats-containers", "Containers", |
| Iterables.transform(nodesInRole, new Function<ClusterNode,Entry<TableContent,String>>() { |
| |
| @Override |
| public Entry<TableContent,String> apply(ClusterNode input) { |
| final String containerId = input.name; |
| |
| if (containerInstances.containsKey(containerId)) { |
| RoleInstance roleInst = containerInstances.get(containerId); |
| if (roleInst.container.getNodeHttpAddress() != null) { |
| return Maps.<TableContent,String> immutableEntry( |
| new TableAnchorContent(containerId, buildNodeUrlForContainer(roleInst.container.getNodeHttpAddress(), containerId)), null); |
| } |
| } |
| return Maps.immutableEntry(new TableContent(input.name), null); |
| } |
| |
| })); |
| |
| ClusterDescription desc = slider.getAppState().getClusterStatus(); |
| Map<String,String> options = desc.getRole(name); |
| Iterable<Entry<TableContent,String>> tableContent; |
| |
| // Generate the pairs of data in the expected form |
| if (null != options) { |
| tableContent = Iterables.transform(options.entrySet(), stringStringPairFunc); |
| } else { |
| // Or catch that we have no options and provide "empty" |
| tableContent = Collections.<Entry<TableContent,String>> emptySet(); |
| } |
| |
| // Generate the options used by this role |
| generateRoleDetails(div, "role-options-wrap", "Role Options", tableContent); |
| |
| // Close the div for this role |
| div._(); |
| } |
| } |
| |
| protected static <T> Function<Entry<String,T>,Entry<TableContent,T>> toTableContentFunction() { |
| return new Function<Entry<String,T>,Entry<TableContent,T>>() { |
| @Override |
| public Entry<TableContent,T> apply(Entry<String,T> input) { |
| return Maps.immutableEntry(new TableContent(input.getKey()), input.getValue()); |
| } |
| }; |
| } |
| |
| protected Map<String,RoleInstance> getContainerInstances(List<RoleInstance> roleInstances) { |
| Map<String,RoleInstance> map = Maps.newHashMapWithExpectedSize(roleInstances.size()); |
| for (RoleInstance roleInstance : roleInstances) { |
| // UUID is the containerId |
| map.put(roleInstance.id, roleInstance); |
| } |
| return map; |
| } |
| |
| /** |
| * Given a div, a name for this data, and some pairs of data, generate a nice HTML table. If contents is empty (of size zero), then a mesage will be printed |
| * that there were no items instead of an empty table. |
| * |
| * @param div |
| * @param detailsName |
| * @param contents |
| */ |
| protected <T1 extends TableContent,T2> void generateRoleDetails(DIV<Hamlet> parent, String divSelector, String detailsName, Iterable<Entry<T1,T2>> contents) { |
| final DIV<DIV<Hamlet>> div = parent.div(divSelector).h3(BOLD, detailsName); |
| |
| int offset = 0; |
| TABLE<DIV<DIV<Hamlet>>> table = null; |
| TBODY<TABLE<DIV<DIV<Hamlet>>>> tbody = null; |
| for (Entry<T1,T2> content : contents) { |
| if (null == table) { |
| table = div.table("ui-widget-content ui-corner-bottom"); |
| tbody = table.tbody(); |
| } |
| |
| TR<TBODY<TABLE<DIV<DIV<Hamlet>>>>> row = tbody.tr(offset % 2 == 0 ? EVEN : ODD); |
| |
| // Defer to the implementation of the TableContent for what the cell should contain |
| content.getKey().printCell(row); |
| |
| // Only add the second column if the element is non-null |
| // This also lets us avoid making a second method if we're only making a one-column table |
| if (null != content.getValue()) { |
| row.td(content.getValue().toString()); |
| } |
| |
| row._(); |
| |
| offset++; |
| } |
| |
| // If we made a table, close it out |
| if (null != table) { |
| tbody._()._(); |
| } else { |
| // Otherwise, throw in a nice "no content" message |
| div.p("no-table-contents")._("None")._(); |
| } |
| |
| // Close out the initial div |
| div._(); |
| } |
| |
| /** |
| * Build a URL from the address:port and container ID directly to the NodeManager service |
| * @param nodeAddress |
| * @param containerId |
| * @return |
| */ |
| protected String buildNodeUrlForContainer(String nodeAddress, String containerId) { |
| StringBuilder sb = new StringBuilder(SCHEME.length() + nodeAddress.length() + PATH.length() + containerId.length()); |
| |
| sb.append(SCHEME).append(nodeAddress).append(PATH).append(containerId); |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Creates a table cell with the provided String as content. |
| */ |
| protected static class TableContent { |
| private String cell; |
| |
| public TableContent(String cell) { |
| this.cell = cell; |
| } |
| |
| public String getCell() { |
| return cell; |
| } |
| |
| /** |
| * Adds a td to the given tr. The tr is not closed |
| * @param tableRow |
| */ |
| public void printCell(TR<?> tableRow) { |
| tableRow.td(this.cell); |
| } |
| } |
| |
| /** |
| * Creates a table cell with an anchor to the given URL with the provided String as content. |
| */ |
| protected static class TableAnchorContent extends TableContent { |
| private String anchorUrl; |
| |
| public TableAnchorContent(String cell, String anchorUrl) { |
| super(cell); |
| this.anchorUrl = anchorUrl; |
| } |
| |
| /* (non-javadoc) |
| * @see org.apache.slider.server.appmaster.web.view.ContainerStatsBlock$TableContent#printCell() |
| */ |
| @Override |
| public void printCell(TR<?> tableRow) { |
| tableRow.td().a(anchorUrl, getCell())._(); |
| } |
| } |
| } |