azkaban-developers

Several changes: -Moving ExecutorServlet as an inner class

8/29/2012 12:38:35 AM

Details

diff --git a/src/java/azkaban/executor/ExecutorManager.java b/src/java/azkaban/executor/ExecutorManager.java
index 74d623f..e3ff56c 100644
--- a/src/java/azkaban/executor/ExecutorManager.java
+++ b/src/java/azkaban/executor/ExecutorManager.java
@@ -2,23 +2,25 @@ package azkaban.executor;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileFilter;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import net.sf.ehcache.config.CacheConfiguration;
+import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.ResponseHandler;
@@ -44,6 +46,7 @@ public class ExecutorManager {
 	private static final String ARCHIVE_DIR = ".archive";
 	private static Logger logger = Logger.getLogger(ExecutorManager.class);
 	private static final long ACCESS_ERROR_THRESHOLD = 60000;
+	private static final int UPDATE_THREAD_MS = 1000;
 	private File basePath;
 
 	private AtomicInteger counter = new AtomicInteger();
@@ -52,8 +55,10 @@ public class ExecutorManager {
 	private String url = "localhost";
 	
 	private ConcurrentHashMap<String, ExecutableFlow> runningFlows = new ConcurrentHashMap<String, ExecutableFlow>();
-	private LinkedList<ExecutableFlow> recentlyFinished = new LinkedList<ExecutableFlow>();
-	private int recentlyFinishedSize = 10;
+	
+	private CacheManager manager = CacheManager.create();
+	private Cache recentFlowsCache;
+	private static final int LIVE_SECONDS = 600;
 	
 	public ExecutorManager(Props props) throws IOException, ExecutorManagerException {
 		basePath = new File(props.getString("execution.directory"));
@@ -66,6 +71,7 @@ public class ExecutorManager {
 				throw new RuntimeException("Execution directory " + basePath + " does not exist and cannot be created.");
 			}
 		}
+		setupCache();
 		
 		File activePath = new File(basePath, ACTIVE_DIR);
 		if(!activePath.exists() && !activePath.mkdirs()) {
@@ -86,6 +92,19 @@ public class ExecutorManager {
 		executingManager.start();
 	}
 	
+	private void setupCache() {
+		CacheConfiguration cacheConfig = new CacheConfiguration("recentFlowsCache",2000)
+				.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.FIFO)
+				.overflowToDisk(false)
+				.eternal(false)
+				.timeToLiveSeconds(LIVE_SECONDS)
+				.diskPersistent(false)
+				.diskExpiryThreadIntervalSeconds(0);
+
+		recentFlowsCache = new Cache(cacheConfig);
+		manager.addCache(recentFlowsCache);
+	}
+	
 	public int getExecutableFlows(String projectId, String flowId, int from, int maxResults, List<ExecutableFlow> output) {
 		String projectPath = projectId + File.separator + flowId;
 		File flowProjectPath = new File(basePath, projectPath);
@@ -116,8 +135,24 @@ public class ExecutorManager {
 		
 		return executionFiles.length;
 	}
-
 	
+	public List<ExecutableFlow> getRecentlyFinishedFlows() {
+		ArrayList<ExecutableFlow> flows = new ArrayList<ExecutableFlow>();
+		for(Element elem : recentFlowsCache.getAll(recentFlowsCache.getKeys()).values()) {
+			if (elem != null) {
+				Object obj = elem.getObjectValue();
+				flows.add((ExecutableFlow)obj);
+			}
+		}
+		
+		return flows;
+	}
+	
+	public List<ExecutableFlow> getRunningFlows() {
+		ArrayList<ExecutableFlow> execFlows = new ArrayList<ExecutableFlow>(runningFlows.values());
+		return execFlows;
+	}
+
 	private void loadActiveExecutions() throws IOException, ExecutorManagerException {
 		File activeFlows = new File(basePath, ACTIVE_DIR);
 		File[] activeFlowDirs = activeFlows.listFiles();
@@ -130,14 +165,14 @@ public class ExecutorManager {
 				ExecutionReference reference = ExecutionReference.readFromDirectory(activeFlowDir);
 				
 				ExecutableFlow flow = this.getFlowFromReference(reference);
+				if (flow == null) {
+					logger.error("Flow " + reference.getExecId() + " not found.");
+				}
 				flow.setLastCheckedTime(System.currentTimeMillis());
 				flow.setSubmitted(true);
 				if (flow != null) {
 					runningFlows.put(flow.getExecutionId(), flow);
 				}
-				else {
-					logger.error("Flow " + reference.getExecId() + " not found.");
-				}
 			}
 			else {
 				logger.info("Path " + activeFlowDir + " not a directory.");
@@ -500,12 +535,13 @@ public class ExecutorManager {
 		}
 		
 		runningFlows.remove(exFlow.getExecutionId());
+		recentFlowsCache.put(new Element(exFlow.getExecutionId(), exFlow));
 		cleanupUnusedFiles(exFlow);
 	}
 	
 	private class ExecutingManagerUpdaterThread extends Thread {
 		private boolean shutdown = false;
-		private int updateTimeMs = 1000;
+		private int updateTimeMs = UPDATE_THREAD_MS;
 		public void run() {
 			while (!shutdown) {
 				ArrayList<ExecutableFlow> flows = new ArrayList<ExecutableFlow>(runningFlows.values());
@@ -527,6 +563,7 @@ public class ExecutorManager {
 					try {
 						responseString = getFlowStatusInExecutor(exFlow);
 					} catch (IOException e) {
+						e.printStackTrace();
 						// Connection issue. Backoff 1 sec.
 						synchronized(this) {
 							try {
diff --git a/src/java/azkaban/utils/GUIUtils.java b/src/java/azkaban/utils/GUIUtils.java
new file mode 100644
index 0000000..b4eff9d
--- /dev/null
+++ b/src/java/azkaban/utils/GUIUtils.java
@@ -0,0 +1,50 @@
+package azkaban.utils;
+
+import org.joda.time.format.DateTimeFormat;
+
+public class GUIUtils {
+	public static final String DATE_TIME_STRING = "YYYY-MM-dd HH:MM:ss";
+	
+	public String formatDate(long timeMS) {
+		if (timeMS == -1) {
+			return "-";
+		}
+		
+		return DateTimeFormat.forPattern(DATE_TIME_STRING).print(timeMS);
+	}
+	
+	public String formatDuration(long startTime, long endTime) {
+		if (startTime == -1) {
+			return "-";
+		}
+		
+		long durationMS;
+		if (endTime == -1) {
+			durationMS = System.currentTimeMillis() - startTime;
+		}
+		else {
+			durationMS = endTime - startTime;
+		}
+		
+		long seconds = durationMS/1000;
+		if (seconds < 60) {
+			return seconds + " sec";
+		}
+		
+		long minutes = seconds / 60;
+		seconds %= 60;
+		if (minutes < 60) {
+			return minutes + "m " + seconds + "s";
+		}
+		
+		long hours = minutes / 60;
+		minutes %= 60;
+		if (hours < 24) {
+			return hours + "h " + minutes + "m " + seconds + "s";
+		}
+		
+		long days = hours / 24;
+		hours %= 24;
+		return days + "d " + hours + "h " + minutes + "m";
+	}
+}
diff --git a/src/java/azkaban/webapp/AzkabanExecutorServer.java b/src/java/azkaban/webapp/AzkabanExecutorServer.java
index 0c189b1..d84ea11 100644
--- a/src/java/azkaban/webapp/AzkabanExecutorServer.java
+++ b/src/java/azkaban/webapp/AzkabanExecutorServer.java
@@ -19,21 +19,31 @@ package azkaban.webapp;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.TimeZone;
 
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 import org.apache.log4j.Logger;
+import org.codehaus.jackson.map.ObjectMapper;
 import org.joda.time.DateTimeZone;
 import org.mortbay.jetty.Server;
 import org.mortbay.jetty.servlet.Context;
 import org.mortbay.jetty.servlet.ServletHolder;
 import org.mortbay.thread.QueuedThreadPool;
 
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutorManagerException;
 import azkaban.executor.FlowRunnerManager;
 import azkaban.utils.Props;
 import azkaban.utils.Utils;
 import azkaban.webapp.servlet.AzkabanServletContextListener;
-import azkaban.webapp.servlet.ExecutorServlet;
 
 import joptsimple.OptionParser;
 import joptsimple.OptionSet;
@@ -66,8 +76,7 @@ public class AzkabanExecutorServer {
 		this.props = props;
 
 		int portNumber = props.getInt("executor.port", DEFAULT_PORT_NUMBER);
-		int maxThreads = props.getInt("executor.maxThreads",
-				DEFAULT_THREAD_NUMBER);
+		int maxThreads = props.getInt("executor.maxThreads", DEFAULT_THREAD_NUMBER);
 
 		Server server = new Server(portNumber);
 		QueuedThreadPool httpThreadPool = new QueuedThreadPool(maxThreads);
@@ -240,4 +249,110 @@ public class AzkabanExecutorServer {
 
 		return null;
 	}
+	
+	public static class ExecutorServlet extends HttpServlet {
+		private static final Logger logger = Logger.getLogger(ExecutorServlet.class.getName());
+		public static final String JSON_MIME_TYPE = "application/json";
+		
+		public enum State {
+			FAILED, SUCCEEDED, RUNNING, WAITING, IGNORED, READY
+		}
+		private String sharedToken;
+		private AzkabanExecutorServer application;
+		private FlowRunnerManager flowRunnerManager;
+		
+		public ExecutorServlet(String token) {
+			super();
+			sharedToken = token;
+		}
+		
+		@Override
+		public void init(ServletConfig config) throws ServletException {
+			application = (AzkabanExecutorServer) config.getServletContext().getAttribute(AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
+
+			if (application == null) {
+				throw new IllegalStateException(
+						"No batch application is defined in the servlet context!");
+			}
+
+			flowRunnerManager = application.getFlowRunnerManager();
+		}
+
+		
+		protected void writeJSON(HttpServletResponse resp, Object obj) throws IOException {
+			resp.setContentType(JSON_MIME_TYPE);
+			ObjectMapper mapper = new ObjectMapper();
+			OutputStream stream = resp.getOutputStream();
+			mapper.writeValue(stream, obj);
+		}
+
+		@Override
+		public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+			HashMap<String,Object> respMap= new HashMap<String,Object>();
+			
+			String token = getParam(req, "sharedToken");
+			if (!token.equals(sharedToken)) {
+				respMap.put("error", "Mismatched token. Will not run.");
+			}
+			else if (!hasParam(req, "action")) {
+				respMap.put("error", "Parameter action not set");
+			}
+			else if (!hasParam(req, "execid")) {
+				respMap.put("error", "Parameter execid not set.");
+			}
+			else {
+				String action = getParam(req, "action");
+				String execid = getParam(req, "execid");
+				
+				// Handle execute
+				if (action.equals("execute")) {
+					String execpath = getParam(req, "execpath");
+					
+					logger.info("Submitted " + execid + " with " + execpath);
+					try {
+						flowRunnerManager.submitFlow(execid, execpath);
+						respMap.put("status", "success");
+					} catch (ExecutorManagerException e) {
+						e.printStackTrace();
+						respMap.put("error", e.getMessage());
+					}
+				}
+				// Handle Status
+				else if (action.equals("status")) {
+					ExecutableFlow flow = flowRunnerManager.getExecutableFlow(execid);
+					if (flow == null) {
+						respMap.put("status", "notfound");
+					}
+					else {
+						respMap.put("status", flow.getStatus().toString());
+					}
+				}
+			}
+
+			writeJSON(resp, respMap);
+			resp.flushBuffer();
+		}
+		
+		@Override
+		public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+			
+		}
+		
+		/**
+		 * Duplicated code with AbstractAzkabanServlet, but ne
+		 */
+		public boolean hasParam(HttpServletRequest request, String param) {
+			return request.getParameter(param) != null;
+		}
+
+		public String getParam(HttpServletRequest request, String name)
+				throws ServletException {
+			String p = request.getParameter(name);
+			if (p == null)
+				throw new ServletException("Missing required parameter '" + name + "'.");
+			else
+				return p;
+		}
+	}
+
 }
diff --git a/src/java/azkaban/webapp/AzkabanWebServer.java b/src/java/azkaban/webapp/AzkabanWebServer.java
index 63deab0..380e52f 100644
--- a/src/java/azkaban/webapp/AzkabanWebServer.java
+++ b/src/java/azkaban/webapp/AzkabanWebServer.java
@@ -43,6 +43,7 @@ import azkaban.user.XmlUserManager;
 import azkaban.utils.Props;
 import azkaban.utils.Utils;
 import azkaban.webapp.servlet.AzkabanServletContextListener;
+import azkaban.webapp.servlet.ExecutionServlet;
 import azkaban.webapp.servlet.FlowExecutorServlet;
 import azkaban.webapp.servlet.IndexServlet;
 import azkaban.webapp.servlet.ProjectManagerServlet;
@@ -112,7 +113,7 @@ public class AzkabanWebServer {
 	 */
 	public AzkabanWebServer(Props props) throws Exception {
 		this.props = props;
-		velocityEngine = configureVelocityEngine(props.getBoolean( VELOCITY_DEV_MODE_PARAM, false));
+		velocityEngine = configureVelocityEngine(props.getBoolean(VELOCITY_DEV_MODE_PARAM, false));
 		sessionCache = new SessionCache(props);
 		userManager = loadUserManager(props);
 		projectManager = loadProjectManager(props);
@@ -346,7 +347,8 @@ public class AzkabanWebServer {
 
 		root.addServlet(new ServletHolder(new ProjectManagerServlet()),"/manager");
 		root.addServlet(new ServletHolder(new FlowExecutorServlet()),"/executor");
-
+		root.addServlet(new ServletHolder(new ExecutionServlet()),"/executions");
+		
 		root.setAttribute(AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY, app);
 
 		try {
diff --git a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index 4d100a5..a2736eb 100644
--- a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -35,6 +35,7 @@ import org.joda.time.DateTime;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
+import azkaban.utils.GUIUtils;
 import azkaban.utils.JSONUtils;
 import azkaban.utils.Props;
 import azkaban.webapp.AzkabanWebServer;
@@ -55,6 +56,8 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
 	public static final String XML_MIME_TYPE = "application/xhtml+xml";
 	public static final String JSON_MIME_TYPE = "application/json";
 
+	private static final GUIUtils utils = new GUIUtils();
+	
 	private AzkabanWebServer application;
 	private String name;
 	private String label;
@@ -257,6 +260,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
 		page.add("azkaban_name", name);
 		page.add("azkaban_label", label);
 		page.add("azkaban_color", color);
+		page.add("utils", utils);
 		page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
 		page.add("currentTime", (new DateTime()).getMillis());
 		if (session != null && session.getUser() != null) {
diff --git a/src/java/azkaban/webapp/servlet/ExecutionServlet.java b/src/java/azkaban/webapp/servlet/ExecutionServlet.java
new file mode 100644
index 0000000..4c3a7cc
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/ExecutionServlet.java
@@ -0,0 +1,98 @@
+package azkaban.webapp.servlet;
+
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.util.List;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutorManager;
+import azkaban.executor.ExecutorManagerException;
+import azkaban.project.Project;
+import azkaban.project.ProjectManager;
+import azkaban.user.User;
+import azkaban.webapp.session.Session;
+
+public class ExecutionServlet extends LoginAbstractAzkabanServlet {
+	private static final long serialVersionUID = 1L;
+	private ProjectManager projectManager;
+	private ExecutorManager executorManager;
+	
+	@Override
+	public void init(ServletConfig config) throws ServletException {
+		super.init(config);
+		projectManager = this.getApplication().getProjectManager();
+		executorManager = this.getApplication().getExecutorManager();
+	}
+	
+	@Override
+	protected void handleGet(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+		if (hasParam(req, "execid")) {
+			handleExecutionFlowPage(req, resp, session);
+		}
+		else {
+			handleExecutionsPage(req, resp, session);
+		}
+	}
+
+	@Override
+	protected void handlePost(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+
+	}
+	
+	private void handleExecutionsPage(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/executionspage.vm");
+		User user = session.getUser();
+		
+		//executorManager.
+		List<ExecutableFlow> runningFlows = executorManager.getRunningFlows();
+		page.add("runningFlows", runningFlows.isEmpty() ? null : runningFlows);
+		
+		List<ExecutableFlow> finishedFlows = executorManager.getRecentlyFinishedFlows();
+		page.add("recentlyFinished", finishedFlows.isEmpty() ? null : finishedFlows);
+		page.render();
+	}
+	
+	private void handleExecutionFlowPage(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
+		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/executingflowpage.vm");
+		User user = session.getUser();
+		String execId = getParam(req, "execid");
+		page.add("execid", execId);
+
+		ExecutableFlow flow = null;
+		try {
+			flow = executorManager.getExecutableFlow(execId);
+			if (flow == null) {
+				page.add("errorMsg", "Error loading executing flow " + execId + " not found.");
+				page.render();
+				return;
+			}
+		} catch (ExecutorManagerException e) {
+			page.add("errorMsg", "Error loading executing flow: " + e.getMessage());
+			page.render();
+			return;
+		}
+		
+		String projectId = flow.getProjectId();
+		Project project = null;
+		try {
+			project = projectManager.getProject(flow.getProjectId(), user);
+		} catch (AccessControlException e) {
+			page.add("errorMsg", "Do not have permission to view '" + flow.getExecutionId() + "'.");
+			page.render();
+		}
+		
+		if (project == null) {
+			page.add("errorMsg", "Project " + projectId + " not found.");
+		}
+		
+		page.add("projectName", projectId);
+		page.add("flowid", flow.getFlowId());
+		
+		page.render();
+	}
+}
diff --git a/src/java/azkaban/webapp/servlet/FlowExecutorServlet.java b/src/java/azkaban/webapp/servlet/FlowExecutorServlet.java
index 4ce24fe..957b6e0 100644
--- a/src/java/azkaban/webapp/servlet/FlowExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/FlowExecutorServlet.java
@@ -50,7 +50,7 @@ public class FlowExecutorServlet extends LoginAbstractAzkabanServlet {
 			handleExecutionFlowPage(req, resp, session);
 		}
 	}
-
+	
 	private void handleExecutionFlowPage(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException, IOException {
 		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/executingflowpage.vm");
 		User user = session.getUser();
@@ -111,7 +111,7 @@ public class FlowExecutorServlet extends LoginAbstractAzkabanServlet {
 		}
 		else {
 			String projectName = getParam(req, "project");
-	
+			
 			ret.put("project", projectName);
 			if (ajaxName.equals("executeFlow")) {
 				ajaxExecuteFlow(req, resp, ret, session.getUser());
diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index 150cfeb..5ede56c 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -43,7 +43,7 @@
 
 				<div id="all-jobs-content">
 					<div class="section-hd flow-header">
-						<h2><a href="${context}/executor?execid=${execid}">Flow Execution <span>$execid</span></a></h2>
+						<h2><a href="${context}/executions?execid=${execid}">Flow Execution <span>$execid</span></a></h2>
 						<div class="section-sub-hd">
 							<h4><a href="${context}/manager?project=${projectName}">Project <span>$projectName</span></a></h4>
 							<h4 class="separator">&gt;</h4>
@@ -74,7 +74,7 @@
 							</div>
 						</div>
 					</div>
-					<div id="jobListView">
+					<div id="jobListView" class="executionInfo">
 						<table>
 							<thead>
 								<tr>
diff --git a/src/java/azkaban/webapp/servlet/velocity/executionspage.vm b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
new file mode 100644
index 0000000..4a15e8b
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
@@ -0,0 +1,104 @@
+<!DOCTYPE html> 
+<html>
+	<head>
+#parse( "azkaban/webapp/servlet/velocity/style.vm" )
+		<script type="text/javascript" src="${context}/js/jquery/jquery.js"></script>    
+		<script type="text/javascript" src="${context}/js/namespace.js"></script>
+		<script type="text/javascript" src="${context}/js/underscore-1.2.1-min.js"></script>
+		<script type="text/javascript" src="${context}/js/backbone-0.5.3-min.js"></script>
+		<script type="text/javascript" src="${context}/js/jquery.simplemodal.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.main.view.js"></script>
+		<script type="text/javascript">
+			var contextURL = "${context}";
+			var currentTime = ${currentTime};
+			var timezone = "${timezone}";
+			var errorMessage = ${error_message};
+			var successMessage = ${success_message};
+		</script>
+	</head>
+	<body>
+		#set($current_page="executing")
+#parse( "azkaban/webapp/servlet/velocity/nav.vm" )
+		<div class="messaging"><p id="messageClose">X</p><p id="message"></p></div>  
+
+		<div class="content">
+			<div id="all-jobs-content">
+				<div class="section-hd">
+					<h2>Executing Flows</h2>
+				</div>
+			</div>
+			
+			<h3 class="subhead">Currently Running Jobs</h3>
+			<div class="executionInfo">
+				<table id="executingJobs">
+					<thead>
+						<tr>
+							<th>Flow</th>
+							<th>User</th>
+							<th class="date">Start Time</th>
+							<th class="date">End Time</th>
+							<th class="elapse">Elapsed</th>
+							<th class="status">Status</th>
+							<th class="action">Action</th>
+						</tr>
+					</thead>
+					<tbody>
+						#if($runningFlows)
+#foreach($flow in $runningFlows)
+						<tr class="row" >
+							<td class="tb-name">
+								<a href="${context}/execution?execid=${flow.executionId}">${flow.flowId}</a>
+							</td>
+							<td>${flow.submitUser}</td>
+							<td>$utils.formatDate(${flow.startTime})</td>
+							<td>$utils.formatDate(${flow.endTime})</td>
+							<td>$utils.formatDuration(${flow.startTime}, ${flow.endTime})</td>
+							<td>${flow.status}</td>
+							<td></td>
+						</tr>
+#end
+#else
+						<tr><td class="last">No Executing Flows</td></tr>
+#end
+					</tbody>
+				</table>
+			</div>
+			<h3 class="subhead">Recently Finished Jobs</h3>
+			<div class="executionInfo">
+				<table id="recentlyFinished">
+					<thead>
+						<tr>
+							<th>Flow</th>
+							<th>User</th>
+							<th class="date">Start Time</th>
+							<th class="date">End Time</th>
+							<th class="elapse">Elapsed</th>
+							<th class="status">Status</th>
+							<th class="action">Action</th>
+						</tr>
+					</thead>
+					<tbody>
+						#if($recentlyFinished)
+#foreach($flow in $recentlyFinished)
+						<tr class="row" >
+							<td class="tb-name">
+								<a href="${context}/executions?execid=${flow.executionId}">${flow.flowId}</a>
+							</td>
+							<td>${flow.submitUser}</td>
+							<td>$utils.formatDate(${flow.startTime})</td>
+							<td>$utils.formatDate(${flow.endTime})</td>
+							<td>$utils.formatDuration(${flow.startTime}, ${flow.endTime})</td>
+							<td>${flow.status}</td>
+							<td></td>
+						</tr>
+#end
+#else
+						<tr><td class="last">No Recently Finished</td></tr>
+#end	
+					</tbody>
+				</table>
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index f0853ff..90aed86 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -3,7 +3,8 @@
 	<head>
 #parse( "azkaban/webapp/servlet/velocity/style.vm" )
 		<script type="text/javascript" src="${context}/js/jquery/jquery.js"></script>    
-		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui.custom.min.js"></script>   
+		<script type="text/javascript" src="${context}/js/jqueryui/jquery-ui.custom.min.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>  
 		<script type="text/javascript" src="${context}/js/namespace.js"></script>
 		<script type="text/javascript" src="${context}/js/underscore-1.2.1-min.js"></script>
 		<script type="text/javascript" src="${context}/js/backbone-0.5.3-min.js"></script>
@@ -74,17 +75,17 @@
 						</div>
 					</div>
 					<div id="executionsView">
-						<div id="executionDiv" class="all-jobs">
+						<div id="executionDiv" class="all-jobs executionInfo">
 							<table id="execTable">
 								<thead>
 									<tr>
 										<th>Execution Id</th>
 										<th>User</th>
-										<th>Start Time</th>
-										<th>End Time</th>
-										<th>Elapsed</th>
-										<th>Status</th>
-										<th>Action</th>
+										<th class="date">Start Time</th>
+										<th class="date">End Time</th>
+										<th class="elapse">Elapsed</th>
+										<th class="status">Status</th>
+										<th class="action">Action</th>
 									</tr>
 								</thead>
 								<tbody id="execTableBody">
diff --git a/src/java/azkaban/webapp/servlet/velocity/nav.vm b/src/java/azkaban/webapp/servlet/velocity/nav.vm
index c339199..fd9a236 100644
--- a/src/java/azkaban/webapp/servlet/velocity/nav.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/nav.vm
@@ -3,7 +3,7 @@
 			<ul id="nav" class="nav">
 				<li id="all-jobs-tab" #if($current_page == 'all')class="selected"#end><a href="/#all">Projects</a></li>
 				<li id="scheduled-jobs-tab" #if($current_page == 'schedule')class="scheduled"#end><a href="$!context/schedule">Scheduled</a></li>
-				<li id="executing-jobs-tab" #if($current_page == 'executing')class="selected"#end><a href="$!context/executing">Executing</a></li>
+				<li id="executing-jobs-tab" #if($current_page == 'executing')class="selected"#end><a href="$!context/executions">Executing</a></li>
 				<li id="history-jobs-tab" #if($current_page == 'history')class="selected"#end><a href="$!context/history">History</a></li>
 				<li><a href="$!context/fs">HDFS</a></li>
 			</ul>
diff --git a/src/web/css/azkaban.css b/src/web/css/azkaban.css
index a65867a..e183624 100644
--- a/src/web/css/azkaban.css
+++ b/src/web/css/azkaban.css
@@ -1442,34 +1442,38 @@ span.sublabel {
 	font-weight: bold;
 }
 
-#jobListView table th.date {
+.executionInfo table th.date {
 	width: 140px;
 }
 
-#jobListView table th.elapse {
+.executionInfo table th.elapse {
 	width: 90px;
 }
 
-#jobListView table th.status {
+.executionInfo table th.status {
 	width: 100px;
 }
 
-#jobListView table th.logs {
+.executionInfo table th.logs {
 	width: 10px;
 }
 
-#jobListView table th.timeline {
+.executionInfo table th.timeline {
 	width: 280px;
 }
 
-#jobListView table td.timeline {
+.executionInfo table th.action {
+	width: 20px;
+}
+
+.executionInfo table td.timeline {
 	padding: 0px;
 	height: 100%;
 	vertical-align: bottom;
 	margin: 0px;
 }
 
-#jobListView table td {
+.executionInfo table td {
 	padding-left: 6px;
 	height: 20px;
 }
@@ -1513,6 +1517,12 @@ span.sublabel {
     background: linear-gradient(top, #009FC9 0, #007b9b 100%);
 }
 
+h3.subhead {
+	margin: 15px 20px 8px 20px;
+	font-size: 14pt;
+	font-weight: bold;
+}
+
 /* old styles */
 
 .azkaban-charts .hitarea {
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 22a4154..fe4645a 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -541,7 +541,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			
 			var tdId = document.createElement("td");
 			var execA = document.createElement("a");
-			$(execA).attr("href", contextURL + "/executor?execid=" + executions[i].execId);
+			$(execA).attr("href", contextURL + "/executions?execid=" + executions[i].execId);
 			$(execA).text(executions[i].execId);
 			tdId.appendChild(execA);
 			row.appendChild(tdId);
@@ -550,16 +550,32 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			$(tdUser).text(executions[i].submitUser);
 			row.appendChild(tdUser);
 			
+			var startTime = "-";
+			if (executions[i].startTime != -1) {
+				var startDateTime = new Date(executions[i].startTime);
+				startTime = getDateFormat(startDateTime);
+			}
+
 			var tdStartTime = document.createElement("td");
-			$(tdStartTime).text(executions[i].startTime);
+			$(tdStartTime).text(startTime);
 			row.appendChild(tdStartTime);
 			
+			var endTime = "-";
+			var lastTime = executions[i].endTime;
+			if (executions[i].endTime != -1) {
+				var endDateTime = new Date(executions[i].endTime);
+				endTime = getDateFormat(endDateTime);
+			}
+			else {
+				lastTime = (new Date()).getTime();
+			}
+
 			var tdEndTime = document.createElement("td");
-			$(tdEndTime).text(executions[i].endTime);
+			$(tdEndTime).text(endTime);
 			row.appendChild(tdEndTime);
 			
 			var tdElapsed = document.createElement("td");
-			$(tdElapsed).text(executions[i].endTime - executions[i].startTime);
+			$(tdElapsed).text( getDuration(executions[i].startTime, lastTime));
 			row.appendChild(tdElapsed);
 			
 			var tdStatus = document.createElement("td");