azkaban-aplcache
Changes
src/java/azkaban/executor/ExecutorManager.java 59(+48 -11)
src/java/azkaban/utils/GUIUtils.java 50(+50 -0)
src/java/azkaban/webapp/AzkabanExecutorServer.java 121(+118 -3)
src/web/css/azkaban.css 24(+17 -7)
src/web/js/azkaban.flow.view.js 24(+20 -4)
Details
src/java/azkaban/executor/ExecutorManager.java 59(+48 -11)
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 {
src/java/azkaban/utils/GUIUtils.java 50(+50 -0)
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";
+ }
+}
src/java/azkaban/webapp/AzkabanExecutorServer.java 121(+118 -3)
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">></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>
src/web/css/azkaban.css 24(+17 -7)
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 {
src/web/js/azkaban.flow.view.js 24(+20 -4)
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");