/**
* 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.cassandra.service;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.RuntimeMXBean;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.CalloutDeployMessage;
import org.apache.cassandra.db.CalloutManager;
import org.apache.cassandra.db.ColumnFamily;
import org.apache.cassandra.db.RowMutation;
import org.apache.cassandra.db.Table;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.net.EndPoint;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.http.ColumnFamilyFormatter;
import org.apache.cassandra.net.http.HTMLFormatter;
import org.apache.cassandra.net.http.HttpConnection;
import org.apache.cassandra.net.http.HttpRequest;
import org.apache.cassandra.net.http.HttpWriteResponse;
import org.apache.cassandra.procedures.GroovyScriptRunner;
import org.apache.cassandra.utils.LogUtil;
/*
* This class handles the incoming HTTP request after
* it has been parsed.
* Author : Avinash Lakshman ( alakshman@facebook.com) & Prashant Malik ( pmalik@facebook.com )
*/
public class HttpRequestVerbHandler implements IVerbHandler
{
private static final Logger logger_ = Logger.getLogger(HttpRequestVerbHandler.class);
/* These are the list of actions supported */
private static final String DETAILS = "details";
private static final String LOADME = "loadme";
private static final String KILLME = "killme";
private static final String COMPACTME = "compactme";
private static final String LB_HEALTH_CHECK = "lb_health_check";
private static final String LB_HEALTH_CHECK_RESPONSE = "I-AM-ALIVE";
private static final String QUERY = "query";
private static final String INSERT = "insert";
private static final String SCRIPT = "script";
private static final String QUERYRESULTSDIV = "queryResultsDiv";
private static final String INSERTRESULTSDIV = "insertResultsDiv";
private static final String SCRIPTRESULTSDIV = "insertResultsDiv";
private static final String JS_UPDATE_QUERY_FUNCTION = "updateQueryResults";
private static final String JS_UPDATE_INSERT_FUNCTION = "updateInsertResults";
private StorageService storageService_;
public HttpRequestVerbHandler(StorageService storageService)
{
storageService_ = storageService;
}
public void doVerb(Message message)
{
HttpConnection.HttpRequestMessage httpRequestMessage = (HttpConnection.HttpRequestMessage)message.getMessageBody()[0];
try
{
HttpRequest httpRequest = httpRequestMessage.getHttpRequest();
HttpWriteResponse httpServerResponse = new HttpWriteResponse(httpRequest);
if(httpRequest.getMethod().toUpperCase().equals("GET"))
{
// handle the get request type
doGet(httpRequest, httpServerResponse);
}
else if(httpRequest.getMethod().toUpperCase().equals("POST"))
{
// handle the POST request type
doPost(httpRequest, httpServerResponse);
}
// write the response we have constructed into the socket
ByteBuffer buffer = httpServerResponse.flush();
httpRequestMessage.getHttpConnection().write(buffer);
}
catch(Exception e)
{
logger_.warn(LogUtil.throwableToString(e));
}
}
private void doGet(HttpRequest httpRequest, HttpWriteResponse httpResponse)
{
boolean fServeSummary = true;
HTMLFormatter formatter = new HTMLFormatter();
String query = httpRequest.getQuery();
/*
* we do not care about the path for most requests except those
* from the load balancer
*/
String path = httpRequest.getPath();
/* for the health checks, just return the string only */
if(path.contains(LB_HEALTH_CHECK))
{
httpResponse.println(handleLBHealthCheck());
return;
}
formatter.startBody(true, getJSFunctions(), true, true);
formatter.appendLine("<h1><font color=\"white\"> Cluster map </font></h1>");
StringBuilder sbResult = new StringBuilder();
do
{
if(query.contains(DETAILS))
{
fServeSummary = false;
sbResult.append(handleNodeDetails());
break;
}
else if(query.contains(LOADME))
{
sbResult.append(handleLoadMe());
break;
}
else if(query.contains(KILLME))
{
sbResult.append(handleKillMe());
break;
}
else if(query.contains(COMPACTME))
{
sbResult.append(handleCompactMe());
break;
}
}
while(false);
//formatter.appendLine("<br>-------END DEBUG INFO-------<br><br>");
if(fServeSummary)
{
formatter.appendLine(handlePageDisplay(null, null, null));
}
formatter.appendLine("<br>");
if(sbResult.toString() != null)
{
formatter.appendLine(sbResult.toString());
}
formatter.endBody();
httpResponse.println(formatter.toString());
}
/*
* As a result of the POST query, we currently only send back some
* javascript that updates the data in some place on the browser.
*/
private void doPost(HttpRequest httpRequest, HttpWriteResponse httpResponse)
{
String query = httpRequest.getQuery();
HTMLFormatter formatter = new HTMLFormatter();
formatter.startBody(true, getJSFunctions(), true, true);
formatter.appendLine("<h1><font color=\"white\"> Cluster map </font></h1>");
// write a shell for adding some javascript to do in-place updates
StringBuilder sbResult = new StringBuilder();
do
{
if(query.contains(QUERY))
{
sbResult.append(handleQuery(httpRequest));
break;
}
else if(query.contains(INSERT))
{
sbResult.append(handleInsert(httpRequest));
break;
}
else if(query.contains(SCRIPT))
{
sbResult.append(handleScript(httpRequest));
break;
}
}
while(false);
if(sbResult.toString() != null)
{
formatter.appendLine(sbResult.toString());
}
formatter.endBody();
httpResponse.println(formatter.toString());
}
private String handleNodeDetails()
{
HTMLFormatter formatter = new HTMLFormatter();
formatter.appendLine("Token: " + storageService_.getToken());
RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
formatter.appendLine("Up time (in seconds): " + (runtimeMxBean.getUptime()/1000));
MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean();
MemoryUsage memUsage = memoryMxBean.getHeapMemoryUsage();
java.text.DecimalFormat df = new java.text.DecimalFormat("#0.00");
String smemUsed = df.format((double)memUsage.getUsed()/(1024 * 1024));
String smemMax = df.format((double)memUsage.getMax()/(1024 * 1024));
formatter.appendLine("Heap memory usage (in MB): " + smemUsed + "/" + smemMax);
formatter.appendLine("<br>");
formatter.appendLine("<br>");
/*
* Display DB statatics if we have something to show.
*/
displayDBStatistics(formatter, df);
formatter.appendLine("<button onClick=\"window.location='" + StorageService.getHostUrl() + "?" + LOADME + "=T'\">Load Me</button>");
formatter.appendLine("<button onClick=\"window.location='" + StorageService.getHostUrl() + "?" + COMPACTME + "=T'\">Compact Me</button>");
formatter.appendLine("<button onClick=\"window.location='" + StorageService.getHostUrl() + "?" + KILLME + "=T'\">Kill Me</button>");
formatter.appendLine("<br>");
formatter.appendLine("<br><a href='" + StorageService.getHostUrl() + "'>Back to live nodes list" + "</a>");
return formatter.toString();
}
private void displayDBStatistics(HTMLFormatter formatter, java.text.DecimalFormat df)
{
String tableStats = Table.open( DatabaseDescriptor.getTables().get(0) ).tableStats("\n<br>\n", df);
if ( tableStats.length() == 0 )
return;
formatter.appendLine("DB statistics:");
formatter.appendLine("<br>");
formatter.appendLine("<br>");
formatter.appendLine(tableStats);
formatter.appendLine("<br>");
formatter.appendLine("<br>");
}
private String handlePageDisplay(String queryFormData, String insertFormData, String scriptFormData)
{
StringBuilder sb = new StringBuilder();
sb.append("\n<div id=\"header\"> \n");
sb.append("<ul>\n");
sb.append(" <li name=\"one\" onclick=\"javascript:selectTab('one')\"><a href=\"#\">Cluster</a></li>\n");
sb.append(" <li name=\"two\" onclick=\"javascript:selectTab('two')\"><a href=\"#\">SQL</a></li>\n");
sb.append(" <li name=\"three\" onclick=\"javascript:selectTab('three')\"><a href=\"#\">Ring</a></li>\n");
sb.append("</ul>\n");
sb.append("</div>\n\n");
sb.append("<div name=\"one\" id=\"content\"> <!-- start tab one -->\n\n");
sb.append(serveSummary());
sb.append("</div> <!-- finish tab one -->\n\n");
sb.append("<div name=\"two\" id=\"content\"> <!-- start tab two -->\n\n");
sb.append(serveInsertForm(insertFormData));
sb.append(serveQueryForm(queryFormData));
sb.append(serveGroovyForm(scriptFormData));
sb.append("</div> <!-- finish tab two -->\n\n");
sb.append("<div name=\"three\" id=\"content\"> <!-- start tab three -->\n\n");
sb.append(serveRingView());
sb.append("</div> <!-- finish tab three -->\n\n");
sb.append("\n<script type=\"text/javascript\">\n");
if(queryFormData != null || insertFormData != null || scriptFormData != null)
sb.append("selectTab(\"two\");\n");
else
sb.append("selectTab(\"one\");\n");
sb.append("</script>\n");
return (sb.toString() == null)?"":sb.toString();
}
/*
* Serve the summary of the current node.
*/
private String serveSummary()
{
HTMLFormatter formatter = new HTMLFormatter();
Set<EndPoint> liveNodeList = Gossiper.instance().getAllMembers();
// we want this set of live nodes sorted based on the hostname
EndPoint[] liveNodes = liveNodeList.toArray(new EndPoint[0]);
Arrays.sort(liveNodes);
String[] sHeaders = {"Node No.", "Host:Port", "Status", "Leader", "Load Info", "Token", "Generation No."};
formatter.startTable();
formatter.addHeaders(sHeaders);
int iNodeNumber = 0;
for( EndPoint curNode : liveNodes )
{
formatter.startRow();
++iNodeNumber;
// Node No.
formatter.addCol("" + iNodeNumber);
// Host:Port
formatter.addCol("<a href='http://" + curNode.getHost() + ":" + DatabaseDescriptor.getHttpPort() + "/home?" + DETAILS + "=T'>" + curNode.getHost() + ":" + curNode.getPort() + "</a>");
//Status
String status = ( FailureDetector.instance().isAlive(curNode) ) ? "Up" : "Down";
formatter.addCol(status);
//Leader
boolean isLeader = StorageService.instance().isLeader(curNode);
formatter.addCol(Boolean.toString(isLeader));
//Load Info
String loadInfo = getLoadInfo(curNode);
formatter.addCol(loadInfo);
// Token
if(curNode == null)
formatter.addCol("NULL!");
else
formatter.addCol(storageService_.getToken(curNode));
// Generation Number
formatter.addCol(Integer.toString(Gossiper.instance().getCurrentGenerationNumber(curNode)));
formatter.endRow();
}
formatter.endTable();
return formatter.toString();
}
private String serveRingView()
{
HTMLFormatter formatter = new HTMLFormatter();
String[] sHeaders = {"Range No.", "Range", "N1", "N2", "N3"};
formatter.startTable();
formatter.addHeaders(sHeaders);
Map<Range, List<EndPoint>> oldRangeToEndPointMap = StorageService.instance().getRangeToEndPointMap();
Set<Range> rangeSet = oldRangeToEndPointMap.keySet();
int iNodeNumber = 0;
for ( Range range : rangeSet )
{
formatter.startRow();
++iNodeNumber;
// Range No.
formatter.addCol("" + iNodeNumber);
// Range
formatter.addCol("(" + range.left() + ",<br>" + range.right() + "]");
List<EndPoint> replicas = oldRangeToEndPointMap.get(range);
for ( EndPoint replica : replicas )
{
// N1 N2 N3
formatter.addCol(replica.toString());
}
formatter.endRow();
}
formatter.endTable();
return formatter.toString();
}
private String getLoadInfo(EndPoint ep)
{
if ( StorageService.getLocalControlEndPoint().equals(ep) )
{
return StorageService.instance().getLoadInfo();
}
else
{
return StorageService.instance().getLoadInfo(ep);
}
}
/*
* Returns the HTML code for a form to query data from the db cluster.
*/
private String serveQueryForm(String queryResult)
{
HTMLFormatter formatter = new HTMLFormatter();
formatter.appendLine("<BR><fieldset><legend>Query the cluster</legend>");
formatter.appendLine("<FORM action=\"" + StorageService.getHostUrl() + "/home?" + QUERY + "=T\" method=\"post\">");
// get the list of column families
Table table = Table.open("Mailbox");
Set<String> columnFamilyComboBoxSet = table.getColumnFamilies();
formatter.append("select from ");
formatter.addCombobox(columnFamilyComboBoxSet, "columnfamily", 0);
formatter.append(" : <INPUT name=columnName>");
formatter.appendLine(" where key = <INPUT name=key>");
formatter.appendLine("<BR>");
formatter.appendLine("<INPUT type=\"submit\" value=\"Send\"> <INPUT type=\"reset\">");
formatter.appendLine("</FORM>");
formatter.addDivElement(QUERYRESULTSDIV, queryResult);
formatter.appendLine("</fieldset><BR>");
return formatter.toString();
}
/*
* Returns the HTML code for a form to to run custom code on the cluster.
*/
private String serveGroovyForm(String scriptResult)
{
HTMLFormatter formatter = new HTMLFormatter();
formatter.appendLine("<BR><fieldset><legend>Run custom code on the cluster</legend>");
formatter.appendLine("<FORM action=\"" + StorageService.getHostUrl() + "/home?" + SCRIPT + "=T\" method=\"post\">");
formatter.append(" Callout name : <INPUT name=calloutName>");
formatter.appendLine("<BR>");
formatter.append("Groovy code to run on the server:<br>");
formatter.append("<textarea name=scriptTextArea rows=\"10\" cols=\"100\"></textarea>");
formatter.appendLine("<BR>");
formatter.appendLine("<INPUT name=deploy type=\"submit\" value=\"Deploy\"> <INPUT name=execute type=\"submit\" value=\"Execute\"> <INPUT name=reset type=\"reset\">");
formatter.appendLine("</FORM>");
formatter.addDivElement(SCRIPTRESULTSDIV, scriptResult);
formatter.appendLine("</fieldset><BR>");
return formatter.toString();
}
/*
* Returns the HTML code for a form to insert data into the db cluster.
*/
private String serveInsertForm(String insertResult)
{
HTMLFormatter formatter = new HTMLFormatter();
formatter.appendLine("<BR><fieldset>\n<legend>Insert data into the cluster</legend>\n");
formatter.appendLine("<FORM action=\"" + StorageService.getHostUrl() + "/home?" + INSERT + "=T\" method=\"post\">");
// get the list of column families
Table table = Table.open("Mailbox");
Set<String> columnFamilyComboBoxSet = table.getColumnFamilies();
formatter.append("insert into ");
formatter.addCombobox(columnFamilyComboBoxSet, "columnfamily", 0);
formatter.append(" : <INPUT name=columnName>");
formatter.append(" data = <INPUT name=data>");
formatter.appendLine(" where key = <INPUT name=key>\n");
formatter.appendLine("<BR>\n");
formatter.appendLine("<INPUT type=\"submit\" value=\"Send\"> <INPUT type=\"reset\">\n");
formatter.appendLine("</FORM>\n");
formatter.addDivElement(INSERTRESULTSDIV, insertResult);
formatter.appendLine("</fieldset>\n<BR>\n");
return formatter.toString();
}
/*
* Handle the query of some data from the client.
*/
private String handleQuery(HttpRequest httpRequest)
{
boolean fQuerySuccess = false;
String sRetVal = "";
// get the various values for this HTTP request
String sColumnFamily = httpRequest.getParameter("columnfamily");
String sColumn = httpRequest.getParameter("columnName");
String sKey = httpRequest.getParameter("key");
// get the table name
String sTableName = DatabaseDescriptor.getTables().get(0);
StringBuilder sb = new StringBuilder();
ColumnFamilyFormatter cformatter = new ColumnFamilyFormatter(sb);
try
{
Table table = Table.open(sTableName);
String queryFor = sColumnFamily;
if(sColumn != null && !"*".equals(sColumn))
{
queryFor += ":" + sColumn;
}
ColumnFamily cf = table.get(sKey, queryFor);
if (cf == null)
{
sRetVal = "Key [" + sKey + "], column family [" + sColumnFamily + "] not found.";
}
else
{
cformatter.printKeyColumnFamily(sb, sKey, cf);
fQuerySuccess = true;
sRetVal = sb.toString();
}
}
catch (Exception e)
{
// write failed - return the reason
sRetVal = e.getMessage();
}
if(fQuerySuccess)
sRetVal = "Success: " + sRetVal;
else
sRetVal = "Error: " + sRetVal;
return handlePageDisplay(sRetVal, null, null);
}
/*
* Handle the query of some data from the client.
*/
private String handleInsert(HttpRequest httpRequest)
{
boolean fInsertSuccess = false;
String sRetVal = "";
// get the various values for this HTTP request
String sColumnFamily = httpRequest.getParameter("columnfamily");
String sColumn = httpRequest.getParameter("columnName");
String sKey = httpRequest.getParameter("key");
String sDataToInsert = httpRequest.getParameter("data");
// get the table name
String sTableName = DatabaseDescriptor.getTables().get(0);
try
{
// do the insert first
RowMutation rm = new RowMutation(sTableName, sKey);
rm.add(sColumnFamily + ":" + sColumn, sDataToInsert.getBytes(), 0);
rm.apply();
fInsertSuccess = true;
sRetVal = "columnfamily=" + httpRequest.getParameter("columnfamily") + " key=" + httpRequest.getParameter("key") + " data=" + httpRequest.getParameter("data");
}
catch (Exception e)
{
// write failed - return the reason
sRetVal = e.getMessage();
}
System.out.println("Write done ...");
if(fInsertSuccess)
sRetVal = "The insert was successful : " + sRetVal;
else
sRetVal = "The insert was failed : " + sRetVal;
return handlePageDisplay(null, sRetVal, null);
}
/*
* Handle the script to be run on the server.
*/
private String handleScript(HttpRequest httpRequest)
{
boolean fQuerySuccess = false;
String sRetVal = "";
// get the various values for this HTTP request
String callout = httpRequest.getParameter("calloutName");
String script = httpRequest.getParameter("scriptTextArea");
String deploy = httpRequest.getParameter("deploy");
String execute = httpRequest.getParameter("execute");
try
{
if ( deploy != null )
{
if ( callout != null && script != null )
{
doDeploy(callout, script);
sRetVal = "Finished deployment of callouts ...";
}
}
if ( execute != null )
{
if ( script != null )
{
sRetVal = GroovyScriptRunner.evaluateString(script);
}
}
fQuerySuccess = true;
}
catch(Throwable t)
{
sRetVal = t.getMessage();
logger_.warn(LogUtil.throwableToString(t));
}
if(fQuerySuccess)
sRetVal = "Result: Success<br>\nReturn value: <br>\n" + sRetVal;
else
sRetVal = "Result: Error<br>\nError: <br>\n" + sRetVal;
return handlePageDisplay(null, null, sRetVal);
}
private void doDeploy(String callout, String script)
{
Set<EndPoint> allMbrs = Gossiper.instance().getAllMembers();
/* Send the script to all mbrs to deploy it locally. */
CalloutDeployMessage cdMessage = new CalloutDeployMessage(callout, script);
try
{
Message message = CalloutDeployMessage.getCalloutDeployMessage(cdMessage);
for ( EndPoint mbr : allMbrs )
{
if ( mbr.equals( StorageService.getLocalControlEndPoint() ) )
{
/* Deploy locally */
CalloutManager.instance().addCallout(callout, script);
}
else
{
EndPoint to = new EndPoint(mbr.getHost(), DatabaseDescriptor.getStoragePort());
logger_.debug("Deploying the script to " + mbr);
MessagingService.getMessagingInstance().sendOneWay(message, to);
}
}
}
catch ( IOException ex )
{
logger_.warn( LogUtil.throwableToString(ex) );
}
}
private String getJSFunctions()
{
StringBuilder sb = new StringBuilder();
sb.append("function " + JS_UPDATE_QUERY_FUNCTION + "(text)\n");
sb.append("{\n");
sb.append(" obj = document.getElementById(\"" + QUERYRESULTSDIV + "\");\n");
sb.append(" if(obj)\n");
sb.append(" obj.innerHTML = text;\n");
sb.append("}\n");
sb.append("\n");
sb.append("function " + JS_UPDATE_INSERT_FUNCTION + "(text)\n");
sb.append("{\n");
sb.append(" obj = document.getElementById(\"" + INSERTRESULTSDIV + "\");\n");
sb.append(" if(obj)\n");
sb.append(" obj.innerHTML = text;\n");
sb.append("}\n");
sb.append("\n");
return sb.toString();
}
/*
* Load the current node with data.
*/
private String handleLoadMe()
{
return "Loading...";
}
private String handleCompactMe()
{
Table table = Table.open(DatabaseDescriptor.getTables().get(0));
table.forceCompaction();
return "Compacting ...";
}
private String handleLBHealthCheck()
{
if(StorageService.instance().isShutdown())
return "";
return LB_HEALTH_CHECK_RESPONSE;
}
/*
* Kill the current node.
*/
private String handleKillMe()
{
if(StorageService.instance().isShutdown())
return "Already scheduled for being shutdown";
/*
* The storage service will wait for a period of time to let the
* VIP know that we are shutting down, then will perform an actual
* shutdown on a separate thread.
*/
String status = "Service has been killed";
try
{
StorageService.instance().killMe();
}
catch( Throwable th )
{
logger_.warn(LogUtil.throwableToString(th));
status = "Failed to kill service.";
}
return status;
}
}