azkaban-memoizeit

Adding base embedded flow support

7/25/2013 11:26:14 PM

Changes

src/web/js/azkaban.flow.graph.view.js 281(+0 -281)

unit/build.xml 11(+11 -0)

Details

diff --git a/src/java/azkaban/flow/Node.java b/src/java/azkaban/flow/Node.java
index fbe5a39..03ca850 100644
--- a/src/java/azkaban/flow/Node.java
+++ b/src/java/azkaban/flow/Node.java
@@ -32,6 +32,8 @@ public class Node {
 	private int expectedRunTimeSec = 1;
 	private String type;
 	
+	private String embeddedFlowId;
+	
 	public Node(String id) {
 		this.id = id;
 	}
@@ -102,6 +104,14 @@ public class Node {
 		return expectedRunTimeSec;
 	}
 	
+	public void setEmbeddedFlowId(String flowId) {
+		embeddedFlowId = flowId;
+	}
+	
+	public String getEmbeddedFlowId() {
+		return embeddedFlowId;
+	}
+	
 	@SuppressWarnings("unchecked")
 	public static Node fromObject(Object obj) {
 		Map<String,Object> mapObj = (Map<String,Object>)obj;
@@ -111,10 +121,13 @@ public class Node {
 		String jobSource = (String)mapObj.get("jobSource");
 		String propSource = (String)mapObj.get("propSource");
 		String jobType = (String)mapObj.get("jobType");
-		
+
+		String embeddedFlowId = (String)mapObj.get("embeddedFlowId");
+
 		node.setJobSource(jobSource);
 		node.setPropsSource(propSource);
 		node.setType(jobType);
+		node.setEmbeddedFlowId(embeddedFlowId);
 		
 		Integer expectedRuntime = (Integer)mapObj.get("expectedRuntime");
 		if (expectedRuntime != null) {
@@ -153,6 +166,9 @@ public class Node {
 		objMap.put("jobSource", jobSource);
 		objMap.put("propSource", propsSource);
 		objMap.put("jobType", type);
+		if (embeddedFlowId != null) {
+			objMap.put("embeddedFlowId", embeddedFlowId);
+		}
 		objMap.put("expectedRuntime", expectedRunTimeSec);
 
 		HashMap<String, Object> layoutInfo = new HashMap<String, Object>();
diff --git a/src/java/azkaban/jobExecutor/AbstractJob.java b/src/java/azkaban/jobExecutor/AbstractJob.java
index 4e5f573..74f5a07 100644
--- a/src/java/azkaban/jobExecutor/AbstractJob.java
+++ b/src/java/azkaban/jobExecutor/AbstractJob.java
@@ -28,7 +28,6 @@ public abstract class AbstractJob implements Job {
 	public static final String JOB_FULLPATH = "job.fullpath";
 	public static final String JOB_ID = "job.id";
 	
-	
 	private final String _id;
 	private final Logger _log;
 	private volatile double _progress;
diff --git a/src/java/azkaban/user/User.java b/src/java/azkaban/user/User.java
index d538e72..ee5abbf 100644
--- a/src/java/azkaban/user/User.java
+++ b/src/java/azkaban/user/User.java
@@ -17,6 +17,7 @@
 package azkaban.user;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -25,6 +26,7 @@ public class User {
 	private final String userid;
 	private Set<String> roles = new HashSet<String>();
 	private Set<String> groups = new HashSet<String>();
+	private HashMap<String,String> properties = new HashMap<String,String>();
 	
 	public User(String userid) {
 		this.userid = userid;
@@ -62,6 +64,10 @@ public class User {
 		return roles.contains(role);
 	}
 	
+	public String getProperty(String name) {
+		return properties.get(name);
+	}
+	
 	public String toString() {
 		String groupStr = "[";
 		for (String group: groups) {
diff --git a/src/java/azkaban/utils/DirectoryFlowLoader.java b/src/java/azkaban/utils/DirectoryFlowLoader.java
index bad673c..0343033 100644
--- a/src/java/azkaban/utils/DirectoryFlowLoader.java
+++ b/src/java/azkaban/utils/DirectoryFlowLoader.java
@@ -71,7 +71,8 @@ public class DirectoryFlowLoader {
 		duplicateJobs = new HashSet<String>();
 		nodeDependencies = new HashMap<String, Map<String, Edge>>();
 		rootNodes = new HashSet<String>();
-
+		flowDependencies = new HashMap<String, Set<String>>();
+		
 		// Load all the props files and create the Node objects
 		loadProjectFromDir(baseDirectory.getPath(), baseDirectory, null);
 		
@@ -163,9 +164,12 @@ public class DirectoryFlowLoader {
 	}
 	
 	private void resolveEmbeddedFlow(String flowId, Set<String> visited) {
-		visited.add(flowId);
-		
 		Set<String> embeddedFlow = flowDependencies.get(flowId);
+		if (embeddedFlow == null) {
+			return;
+		}
+		
+		visited.add(flowId);
 		for (String embeddedFlowId: embeddedFlow) {
 			if (visited.contains(embeddedFlowId)) {
 				errors.add("Embedded flow cycle found in " + flowId + "->" + embeddedFlowId);
@@ -305,6 +309,7 @@ public class DirectoryFlowLoader {
 				flowDependencies.put(flow.getId(), embeddedFlows);
 			}
 
+			node.setEmbeddedFlowId(embeddedFlow);
 			embeddedFlows.add(embeddedFlow);
 		}
 		Map<String, Edge> dependencies = nodeDependencies.get(node.getId());
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index 0b2d413..ea5b901 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -513,7 +513,11 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 			HashMap<String, Object> nodeObj = new HashMap<String,Object>();
 			nodeObj.put("id", node.getId());
 			nodeObj.put("level", node.getLevel());
-
+			nodeObj.put("type", node.getType());
+			if (node.getEmbeddedFlowId() != null) {
+				nodeObj.put("flowId", node.getEmbeddedFlowId());
+			}
+			
 			nodeList.add(nodeObj);
 		}
 		
diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index de86051..7a03448 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -47,6 +47,7 @@
 			var execId = "${execid}";
 		</script>
 		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
+		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
 	</head>
 	<body>
 #set($current_page="all")
@@ -98,7 +99,12 @@
 								</div>
 								<div id="list" class="list">
 								</div>
-								<div id="resetPanZoomBtn" class="btn5 resetPanZoomBtn" >Reset Pan Zoom</div>
+								<div id="flowGraphOptions">
+									<div id="autoPanZoom">
+										<input type="checkbox" id="autoPanZoomCheckbox" class="autoPanZoom" value="autoPanZoom" /><label for="autoPanZoom">Auto Pan Zoom</label>
+									</div>
+									<div id="resetPanZoomBtn" class="btn5 resetPanZoomBtn" >Reset Pan Zoom</div>
+								</div>
 							</div>
 							<div id="svgDiv" class="svgDiv">
 								<svg id="svgGraph" class="svgGraph" xmlns="http://www.w3.org/2000/svg" version="1.1" shape-rendering="optimize-speed" text-rendering="optimize-speed" >
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index 6d4ddcb..aab0e61 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -3,6 +3,7 @@
 <script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
 <script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
 <script type="text/javascript" src="${context}/js/azkaban.flow.execute.view.js"></script>
+<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
 
 <div id="modalBackground" class="modalBackground2">
 <div id="execute-flow-panel" class="modal modalContainer2">
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index d1ab610..1bec383 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -31,7 +31,6 @@
 		<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.flow.graph.view.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.view.js"></script>
 		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
 		<script type="text/javascript">
@@ -47,6 +46,7 @@
 			var execId = null;
 		</script>
 		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
+		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
 	</head>
 	<body>
 #set($current_page="all")
@@ -87,7 +87,12 @@
 								</div>
 								<div id="list" class="list">
 								</div>
-								<div id="resetPanZoomBtn" class="btn5 resetPanZoomBtn" >Reset Pan Zoom</div>
+								<div id="flowGraphOptions">
+									<div id="autoPanZoom">
+										<input type="checkbox" id="autoPanZoomCheckbox" class="autoPanZoom" value="autoPanZoom" /><label for="autoPanZoom">Auto Pan Zoom</label>
+									</div>
+									<div id="resetPanZoomBtn" class="btn5 resetPanZoomBtn" >Reset Pan Zoom</div>
+								</div>
 							</div>
 							<div id="svgDiv" class="svgDiv">
 								<svg id="svgGraph" class="svgGraph" xmlns="http://www.w3.org/2000/svg" version="1.1" shape-rendering="optimize-speed" text-rendering="optimize-speed" >
diff --git a/src/java/azkaban/webapp/session/SessionCache.java b/src/java/azkaban/webapp/session/SessionCache.java
index be76c55..09d0b36 100644
--- a/src/java/azkaban/webapp/session/SessionCache.java
+++ b/src/java/azkaban/webapp/session/SessionCache.java
@@ -33,7 +33,7 @@ import azkaban.utils.cache.Element;
  */
 public class SessionCache {
 	private static final int MAX_NUM_SESSIONS = 10000;
-	private static final int SESSION_TIME_TO_LIVE = 10000;
+	private static final int SESSION_TIME_TO_LIVE = 86400000;
 //	private CacheManager manager = CacheManager.create();
 	private Cache cache;
 
diff --git a/src/package/webserver/bin/azkaban-web-start.sh b/src/package/webserver/bin/azkaban-web-start.sh
index bdd19e0..dba3bb9 100755
--- a/src/package/webserver/bin/azkaban-web-start.sh
+++ b/src/package/webserver/bin/azkaban-web-start.sh
@@ -28,7 +28,7 @@ serverpath=`pwd`
 if [ -z $AZKABAN_OPTS ]; then
   AZKABAN_OPTS=-Xmx3G
 fi
-AZKABAN_OPTS=$AZKABAN_OPTS -server -Dcom.sun.management.jmxremote -Djava.io.tmpdir=$tmpdir -Dexecutorport=$executorport -Dserverpath=$serverpath
+AZKABAN_OPTS="$AZKABAN_OPTS -server -Dcom.sun.management.jmxremote -Djava.io.tmpdir=$tmpdir -Dexecutorport=$executorport -Dserverpath=$serverpath"
 
 java $AZKABAN_OPTS -cp $CLASSPATH azkaban.webapp.AzkabanWebServer -conf $azkaban_dir/conf $@ &
 
diff --git a/src/web/css/azkaban.css b/src/web/css/azkaban.css
index aadf527..f7f3fa8 100644
--- a/src/web/css/azkaban.css
+++ b/src/web/css/azkaban.css
@@ -1833,9 +1833,13 @@ tr:hover td {
 	width: 250px;
 }
 
-.resetPanZoomBtn {
+#flowGraphOptions {
 	position: absolute;
-	bottom: 90px;
+	bottom: 70px;
+}
+
+#flowGraphOptions label{
+	font-size: 9pt;
 }
 
 #nodeOptions {
@@ -2135,138 +2139,6 @@ table.parameters tr td {
 	text-align:center;
 }
 
-svg .edge {
-	stroke: #777;
-	stroke-width: 2;
-}
-
-svg .edge:hover {
-	stroke: #009FC9;
-	stroke-width: 4;
-}
-
-svg .node.disabled {
-	opacity: 0.3;
-}
-
-svg .node .backboard {
-	fill: #FFF;
-	opacity: 0.05;
-}
-
-svg .node:hover {
-	cursor: pointer;
-}
-
-svg .node:hover .backboard {
-	opacity: 0.7;
-}
-
-svg .selected .backboard {
-	opacity: 0.4;
-}
-
-svg .node circle {
-	fill: #888;
-	stroke: #777;
-	stroke-width: 2;
-}
-
-svg .node:hover circle {
-	stroke: #009FC9;
-}
-
-svg .node:hover text {
-	fill: #009FC9;
-}
-
-svg .selected text {
-	fill: #338AB0;
-}
-
-svg .selected circle {
-	stroke: #009FC9;
-	stroke-width: 4;
-}
-
-svg .READY circle {
-	fill: #CCC;
-}
-
-svg .RUNNING circle {
-	fill: #009FC9;
-}
-
-svg .QUEUED circle {
-	opacity: 0.5;
-	fill: #009FC9;
-}
-
-svg .FAILED circle {
-	fill: #CC0000;
-}
-
-svg .KILLED circle {
-	fill: #CC0000;
-}
-
-svg .SUCCEEDED circle {
-	fill: #00CC33;
-}
-
-svg .DISABLED circle {
-	opacity: 0.3;
-}
-
-svg .SKIPPED circle {
-	opacity: 0.3;
-}
-
-#Used for charts
-svg circle.READY {
-	stroke: #CCC;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.RUNNING {
-	stroke: #009FC9;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.FAILED {
-	stroke: #CC0000;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.KILLED {
-	stroke: #CC0000;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.SUCCEEDED {
-	stroke: #00CC33;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.DISABLED {
-	stroke: #CCC;
-	opacity: 0.3;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.SKIPPED {
-	stroke: #CCC;
-	opacity: 0.3;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
 span.sublabel {
 	font-size: 8pt;
 	margin-left: 12px;
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
new file mode 100644
index 0000000..c20e6d9
--- /dev/null
+++ b/src/web/css/azkaban-graph.css
@@ -0,0 +1,198 @@
+
+svg .edge {
+	stroke: #BBB;
+	stroke-width: 2;
+}
+
+svg .edge:hover {
+	stroke: #009FC9;
+	stroke-width: 4;
+}
+
+svg .node.disabled {
+	opacity: 0.3;
+}
+
+svg .node .backboard {
+	fill: #FFF;
+	opacity: 0.05;
+}
+
+svg .node:hover {
+	cursor: pointer;
+}
+
+svg .node:hover .backboard {
+	opacity: 0.7;
+}
+
+svg .selected .backboard {
+	opacity: 0.4;
+}
+
+svg .node circle {
+	fill: #888;
+	stroke: #777;
+	stroke-width: 2;
+}
+
+svg .node:hover circle {
+	stroke: #009FC9;
+}
+
+svg .node:hover text {
+	fill: #009FC9;
+}
+
+svg .selected text {
+	fill: #338AB0;
+}
+
+svg .selected circle {
+	stroke: #009FC9;
+	stroke-width: 4;
+}
+
+svg .READY circle {
+	fill: #CCC;
+}
+
+svg .RUNNING circle {
+	fill: #009FC9;
+}
+
+svg .QUEUED circle {
+	opacity: 0.5;
+	fill: #009FC9;
+}
+
+svg .FAILED circle {
+	fill: #CC0000;
+}
+
+svg .KILLED circle {
+	fill: #CC0000;
+}
+
+svg .SUCCEEDED circle {
+	fill: #00CC33;
+}
+
+svg .DISABLED circle {
+	opacity: 0.3;
+}
+
+svg .SKIPPED circle {
+	opacity: 0.3;
+}
+
+svg .selected circle {
+	stroke: #009FC9;
+	stroke-width: 4;
+}
+
+svg .selected .nodebox rect {
+	stroke: #009FC9;
+	stroke-width: 3;
+}
+
+svg .node rect {
+	fill: #F8F8F8;
+	stroke: #CCC;
+	stroke-width: 2;
+}
+
+svg .node .nodebox text {
+	fill: #FFF;
+}
+
+svg .READY .nodebox text {
+	fill: #000;
+}
+
+svg .node:hover rect {
+	stroke: #009FC9;
+}
+
+svg .READY rect {
+	fill: #EEE;
+}
+
+svg .RUNNING rect {
+	fill: #009FC9;
+}
+
+svg .QUEUED rect {
+	opacity: 0.5;
+	fill: #009FC9;
+}
+
+svg .FAILED rect {
+	fill: #CC0000;
+}
+
+svg .KILLED rect {
+	fill: #CC0000;
+}
+
+svg .SUCCEEDED rect {
+	fill: #30ad23;
+}
+
+svg .DISABLED rect {
+	opacity: 0.3;
+}
+
+svg .SKIPPED rect {
+	opacity: 0.3;
+}
+
+svg .nodebox text {
+	fill: #fff;
+}
+
+
+#Used for charts
+svg circle.READY {
+	stroke: #CCC;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.RUNNING {
+	stroke: #009FC9;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.FAILED {
+	stroke: #CC0000;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.KILLED {
+	stroke: #CC0000;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.SUCCEEDED {
+	stroke: #00CC33;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.DISABLED {
+	stroke: #CCC;
+	opacity: 0.3;
+	stroke-width: 2px;
+	fill: #FFF;
+}
+
+svg circle.SKIPPED {
+	stroke: #CCC;
+	opacity: 0.3;
+	stroke-width: 2px;
+	fill: #FFF;
+}
diff --git a/src/web/images/graph-icon.png b/src/web/images/graph-icon.png
new file mode 100644
index 0000000..d315d97
Binary files /dev/null and b/src/web/images/graph-icon.png differ
diff --git a/src/web/js/azkaban.exflow.view.js b/src/web/js/azkaban.exflow.view.js
index bde19da..388c42e 100644
--- a/src/web/js/azkaban.exflow.view.js
+++ b/src/web/js/azkaban.exflow.view.js
@@ -634,7 +634,9 @@ var exNodeClickCallback = function(event) {
 
 	var menu = [	
 			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
+			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+			{break: 1},
+			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
 	];
 
 	contextMenuView.show(event, menu);
@@ -647,7 +649,9 @@ var exJobClickCallback = function(event) {
 
 	var menu = [	
 			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
+			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+			{break: 1},
+			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
 	];
 
 	contextMenuView.show(event, menu);
diff --git a/src/web/js/azkaban.flow.execute.view.js b/src/web/js/azkaban.flow.execute.view.js
index c8aed58..90319c6 100644
--- a/src/web/js/azkaban.flow.execute.view.js
+++ b/src/web/js/azkaban.flow.execute.view.js
@@ -548,7 +548,9 @@ var nodeClickCallback = function(event) {
 									{title: "Descendents", callback: function(){touchDescendents(jobId, true);}},
 									{title: "Disable All", callback: function(){disableAll();}}
 								]
-			}
+			},
+			{break: 1},
+			{title: "Center Job", callback: function() {executableGraphModel.trigger("centerNode", jobId)}}
 	];
 
 	contextMenuView.show(event, menu);
diff --git a/src/web/js/azkaban.flow.job.view.js b/src/web/js/azkaban.flow.job.view.js
index fa14b58..a85f9a2 100644
--- a/src/web/js/azkaban.flow.job.view.js
+++ b/src/web/js/azkaban.flow.job.view.js
@@ -3,6 +3,7 @@ azkaban.JobListView = Backbone.View.extend({
 		"keyup input": "filterJobs",
 		"click li": "handleJobClick",
 		"click .resetPanZoomBtn" : "handleResetPanZoom",
+		"change .autoPanZoom" : "handleAutoPanZoom",
 		"contextmenu li" : "handleContextMenuClick"
 	},
 	initialize: function(settings) {
@@ -192,5 +193,8 @@ azkaban.JobListView = Backbone.View.extend({
 	},
 	handleResetPanZoom: function(evt) {
 		this.model.trigger("resetPanZoom");
+	},
+	handleAutoPanZoom: function(evt) {
+		this.model.set({"autoPanZoom": $(evt.currentTarget).is(':checked')});
 	}
 });
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 9f3536b..46a336f 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -282,7 +282,9 @@ var exNodeClickCallback = function(event) {
 
 	var menu = [	
 			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
+			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+			{break: 1},
+			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
 	];
 
 	contextMenuView.show(event, menu);
@@ -295,7 +297,9 @@ var exJobClickCallback = function(event) {
 
 	var menu = [	
 			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
+			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+			{break: 1},
+			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
 	];
 
 	contextMenuView.show(event, menu);
diff --git a/src/web/js/azkaban.layout.js b/src/web/js/azkaban.layout.js
index 53e7692..209a52b 100644
--- a/src/web/js/azkaban.layout.js
+++ b/src/web/js/azkaban.layout.js
@@ -4,7 +4,7 @@ var degreeRatio = 1/8;
 var maxHeight = 200;
 var cornerGap = 10;
 
-function layoutGraph(nodes, edges) {
+function layoutGraph(nodes, edges, hmargin) {
 	var startLayer = [];
 	var numLayer = 0;
 	var nodeMap = {};
@@ -12,20 +12,17 @@ function layoutGraph(nodes, edges) {
 	var maxLayer = 0;
 	var layers = {};
 	
+	if (!hmargin) {
+		hmargin = 8;
+	}
+	
 	// Assign to layers
 	for (var i = 0; i < nodes.length; ++i) {
 		numLayer = Math.max(numLayer, nodes[i].level);
-		/*
-		if (nodes[i].id.length > maxTextSize) {
-			var label = nodes[i].id.substr(0, reductionSize) + "...";
-			nodes[i].label = label;
-		}
-		else {*/
-			nodes[i].label = nodes[i].id;
-		//}
-		
-		var width = nodes[i].label.length * 10;
-		var node = { id: nodes[i].id, node: nodes[i], level: nodes[i].level, in:[], out:[], width: width, x:0 };
+
+		var width = nodes[i].width ? nodes[i].width : nodes[i].label.length * 11.5 + 4;
+		var height = nodes[i].height ? nodes[i].height : 1;
+		var node = { id: nodes[i].id, node: nodes[i], level: nodes[i].level, in:[], out:[], width: width + hmargin, x:0, height:height };
 		nodeMap[nodes[i].id] = node;
 
 		maxLayer = Math.max(node.level, maxLayer);
@@ -244,15 +241,21 @@ function spreadLayerSmart(layer) {
 function spaceVertically(layers, maxLayer) {
 	var startY = 0;
 	var startLayer = layers[0];
+	var startMaxHeight = 1;
 	for (var i=0; i < startLayer.length; ++i) {
 		startLayer[i].y = startY;
+		startMaxHeight = Math.max(startMaxHeight, startLayer[i].height);
 	}
 	
-	var minHeight = 50;
+	var minHeight = 40;
 	for (var a=1; a <= maxLayer; ++a) {
 		var maxDelta = 0;
 		var layer = layers[a];
+		
+		var layerMaxHeight = 1;
 		for (var i=0; i < layer.length; ++i) {
+			layerMaxHeight = Math.max(layerMaxHeight, layer[i].height);
+
 			for (var j=0; j < layer[i].in.length; ++j) {
 				var upper = layer[i].in[j];
 				var delta = Math.abs(upper.x - layer[i].x);
@@ -264,8 +267,10 @@ function spaceVertically(layers, maxLayer) {
 		console.log("Max " + maxDelta);
 		var calcHeight = maxDelta*degreeRatio;
 		
-		calcHeight = Math.min(calcHeight, maxHeight); 
-		startY += Math.max(calcHeight, minHeight);
+		var newMinHeight = minHeight + startMaxHeight/2 + layerMaxHeight / 2;
+		startMaxHeight = layerMaxHeight;
+
+		startY += Math.max(calcHeight, newMinHeight);
 		for (var i=0; i < layer.length; ++i) {
 			layer[i].y=startY;
 		}
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index 009b4e4..3703e27 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -34,13 +34,14 @@ azkaban.SvgGraphView = Backbone.View.extend({
 	},
 	initialize: function(settings) {
 		this.model.bind('change:selected', this.changeSelected, this);
+		this.model.bind('centerNode', this.centerNode, this);
 		this.model.bind('change:graph', this.render, this);
 		this.model.bind('resetPanZoom', this.resetPanZoom, this);
 		this.model.bind('change:update', this.handleStatusUpdate, this);
 		this.model.bind('change:disabled', this.handleDisabledChange, this);
 		this.model.bind('change:updateAll', this.handleUpdateAllStatus, this);
 		
-		this.graphMargin = settings.graphMargin ? settings.graphMargin : 200;
+		this.graphMargin = settings.graphMargin ? settings.graphMargin : 25;
 		this.svgns = "http://www.w3.org/2000/svg";
 		this.xlinksn = "http://www.w3.org/1999/xlink";
 		
@@ -59,7 +60,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 	},
 	initializeDefs: function(self) {
 		var def = document.createElementNS(svgns, 'defs');
-		def.setAttributeNS(null, "id", "buttonDefs");
+		def.setAttribute("id", "buttonDefs");
 
 		// ArrowHead
 		var arrowHeadMarker = document.createElementNS(svgns, 'marker');
@@ -97,15 +98,23 @@ azkaban.SvgGraphView = Backbone.View.extend({
 	
 		nodes.sort();
 		edges.sort();
-		// layout
-		layoutGraph(nodes, edges);
 		
 		var bounds = {};
 		this.nodes = {};
 		for (var i = 0; i < nodes.length; ++i) {
 			this.nodes[nodes[i].id] = nodes[i];
+			nodes[i].label = nodes[i].id;
+		}
+		
+		this.gNodes = {};
+		for (var i = 0; i < nodes.length; ++i) {
+			this.drawNode(this, nodes[i]);
 		}
 		
+		// layout
+		layoutGraph(nodes, edges, 10);
+		this.moveNodes(bounds);
+		
 		for (var i = 0; i < edges.length; ++i) {
 			var inNodes = this.nodes[edges[i].target].inNodes;
 			if (!inNodes) {
@@ -124,11 +133,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			this.drawEdge(this, edges[i]);
 		}
 		
-		this.gNodes = {};
-		for (var i = 0; i < nodes.length; ++i) {
-			this.drawNode(this, nodes[i], bounds);
-		}
-		
 		this.model.set({"nodes": this.nodes, "edges": edges});
 		
 		var margin = this.graphMargin;
@@ -162,7 +166,12 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		for (var i = 0; i < data.nodes.length; ++i) {
 			var updateNode = data.nodes[i];
 			var g = this.gNodes[updateNode.id];
-			addClass(g, updateNode.status);
+			if (updateNode.status) {
+				addClass(g, updateNode.status);
+			}
+			else {
+				addClass(g, "READY");
+			}
 		}
 	},
 	changeSelected: function(self) {
@@ -182,12 +191,15 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			
 			addClass(g, "selected");
 			
-			var offset = 200;
-			var widthHeight = offset*2;
-			var x = node.x - offset;
-			var y = node.y - offset;
-			
-			$(this.svgGraph).svgNavigate("transformToBox", {x: x, y: y, width: widthHeight, height: widthHeight});
+			console.log(this.model.get("autoPanZoom"));
+			if (this.model.get("autoPanZoom")) {
+				var offset = 150;
+				var widthHeight = offset*2;
+				var x = node.x - offset;
+				var y = node.y - offset;
+				
+				$(this.svgGraph).svgNavigate("transformToBox", {x: x, y: y, width: widthHeight, height: widthHeight});
+			}
 		}
 	},
 	handleStatusUpdate: function(evt) {
@@ -240,33 +252,206 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var startNode = this.nodes[edge.from];
 		var endNode = this.nodes[edge.target];
 		
+		var startPointY = startNode.y + startNode.height/2 - 3;
+		var endPointY = endNode.y - endNode.height/2 + 3;
 		if (edge.guides) {
-			var pointString = "" + startNode.x + "," + startNode.y + " ";
+			var pointString = "" + startNode.x + "," + startPointY + " ";
 
 			for (var i = 0; i < edge.guides.length; ++i ) {
 				edgeGuidePoint = edge.guides[i];
 				pointString += edgeGuidePoint.x + "," + edgeGuidePoint.y + " ";
 			}
 			
-			pointString += endNode.x + "," + endNode.y;
+			pointString += endNode.x + "," + endPointY;
 			var polyLine = document.createElementNS(svgns, "polyline");
-			polyLine.setAttributeNS(null, "class", "edge");
-			polyLine.setAttributeNS(null, "points", pointString);
-			polyLine.setAttributeNS(null, "style", "fill:none;");
-			self.mainG.appendChild(polyLine);
+			polyLine.setAttribute("class", "edge");
+			polyLine.setAttribute("points", pointString);
+			polyLine.setAttribute("style", "fill:none;");
+			$(self.mainG).prepend(polyLine);
 		}
 		else { 
 			var line = document.createElementNS(svgns, 'line');
-			line.setAttributeNS(null, "class", "edge");
-			line.setAttributeNS(null, "x1", startNode.x);
-			line.setAttributeNS(null, "y1", startNode.y);
-			line.setAttributeNS(null, "x2", endNode.x);
-			line.setAttributeNS(null, "y2", endNode.y);
+			line.setAttribute("class", "edge");
+			line.setAttribute("x1", startNode.x);
+			line.setAttribute("y1", startPointY);
+			line.setAttribute("x2", endNode.x);
+			line.setAttribute("y2", endPointY);
 			
-			self.mainG.appendChild(line);
+			$(self.mainG).prepend(line);
 		}
 	},
-	drawNode: function(self, node, bounds) {
+	drawNode: function(self, node) {
+		if (node.type == 'flow') {
+			this.drawFlowNode(self, node);
+		}
+		else {
+			this.drawBoxNode(self,node);
+			//this.drawCircleNode(self,node,bounds);
+		}
+	},
+	moveNodes: function(bounds) {
+		var nodes = this.nodes;
+		var gNodes = this.gNodes;
+		
+		for (var key in nodes) {
+			var node = nodes[key];
+			var gNode = gNodes[node.id];
+			var centerX = node.centerX;
+			var centerY = node.centerY;
+			
+			gNode.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
+			this.addBounds(bounds, {minX:node.x - centerX, minY: node.y - centerY, maxX: node.x + centerX, maxY: node.y + centerY});
+		}
+	},
+	drawFlowNode: function(self, node) {
+		var svg = self.svgGraph;
+		var svgns = self.svgns;
+
+		var height = 26;
+		var nodeG = document.createElementNS(svgns, "g");
+		nodeG.setAttribute("class", "jobnode");
+		nodeG.setAttribute("font-family", "helvetica");
+		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
+		this.gNodes[node.id] = nodeG;
+		
+		var innerG = document.createElementNS(svgns, "g");
+		innerG.setAttribute("class", "nodebox");
+		
+		var rect = document.createElementNS(svgns, 'rect');
+		rect.setAttribute("rx", 3);
+		rect.setAttribute("ry", 5);
+		rect.setAttribute("style", "width:inherit;stroke-opacity:1");
+		//rect.setAttribute("class", "nodecontainer");
+		
+		var text = document.createElementNS(svgns, 'text');
+		var textLabel = document.createTextNode(node.label);
+		text.appendChild(textLabel);
+		text.setAttribute("y", 1);
+		text.setAttribute("height", 10);
+
+		var flowIdText = document.createElementNS(svgns, 'text');
+		var flowIdLabel = document.createTextNode(node.flowId);
+		flowIdText.appendChild(flowIdLabel);
+		flowIdText.setAttribute("y", 11);
+		flowIdText.setAttribute("font-size", 8);
+		
+		var iconHeight = 20;
+		var iconWidth = 21;
+		var iconMargin = 4;
+		var iconNode = document.createElementNS(svgns, 'image');
+		iconNode.setAttribute("width", iconHeight);
+		iconNode.setAttribute("height", iconWidth);
+		iconNode.setAttribute("x", 0);
+		iconNode.setAttribute("y", -10);
+		iconNode.setAttributeNS('http://www.w3.org/1999/xlink', "xlink:href", contextURL + "/images/graph-icon.png");
+		
+		innerG.appendChild(rect);
+		innerG.appendChild(text);
+		innerG.appendChild(flowIdText);
+		innerG.appendChild(iconNode);
+		innerG.jobid = node.id;
+
+		nodeG.appendChild(innerG);
+		self.mainG.appendChild(nodeG);
+
+		var horizontalMargin = 8;
+		var verticalMargin = 2;
+		
+		// Need to get text width after attaching to SVG.
+		var subLabelTextLength = flowIdText.getComputedTextLength();
+		
+		var computeTextLength = text.getComputedTextLength();
+		var computeTextHeight = 22;
+		
+		var width = computeTextLength > subLabelTextLength ? computeTextLength : subLabelTextLength;
+		width += iconWidth + iconMargin;
+		var halfWidth = width/2;
+		var halfHeight = height/2;
+		
+		// Margin for surrounding box.
+		var boxWidth = width + horizontalMargin * 2;
+		var boxHeight = height + verticalMargin * 2;
+		
+		node.width = boxWidth;
+		node.height = boxHeight;
+		node.centerX = boxWidth/2;
+		node.centerY = boxHeight/2;
+		
+		var textXOffset = -halfWidth + iconWidth + iconMargin;
+		iconNode.setAttribute("x", -halfWidth);
+		text.setAttribute("x", textXOffset);
+		flowIdText.setAttribute("x",textXOffset);
+		rect.setAttribute("x", -node.centerX);
+		rect.setAttribute("y", -node.centerY);
+		rect.setAttribute("width", node.width);
+		rect.setAttribute("height", node.height);
+		
+		nodeG.setAttribute("class", "node");
+		nodeG.jobid=node.id;
+	},
+	drawBoxNode: function(self, node) {
+		var svg = self.svgGraph;
+		var svgns = self.svgns;
+
+		var height = 18;
+		var nodeG = document.createElementNS(svgns, "g");
+		nodeG.setAttribute("class", "jobnode");
+		nodeG.setAttribute("font-family", "helvetica");
+		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
+		this.gNodes[node.id] = nodeG;
+		
+		var innerG = document.createElementNS(svgns, "g");
+		innerG.setAttribute("class", "nodebox");
+		
+		var rect = document.createElementNS(svgns, 'rect');
+		rect.setAttribute("rx", 3);
+		rect.setAttribute("ry", 5);
+		rect.setAttribute("style", "width:inherit;stroke-opacity:1");
+		//rect.setAttribute("class", "nodecontainer");
+		
+		var text = document.createElementNS(svgns, 'text');
+		var textLabel = document.createTextNode(node.label);
+		text.appendChild(textLabel);
+		text.setAttribute("y", 6);
+		text.setAttribute("height", 10); 
+
+		//this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
+
+		innerG.appendChild(rect);
+		innerG.appendChild(text);
+		innerG.jobid = node.id;
+
+		nodeG.appendChild(innerG);
+		self.mainG.appendChild(nodeG);
+
+		var horizontalMargin = 8;
+		var verticalMargin = 2;
+		
+		// Need to get text width after attaching to SVG.
+		var computeText = text.getComputedTextLength();
+		var computeTextHeight = 22;
+		var halfWidth = computeText/2;
+		var halfHeight = height/2;
+		
+		// Margin for surrounding box.
+		var boxWidth = computeText + horizontalMargin * 2;
+		var boxHeight = height + verticalMargin * 2;
+		
+		node.width = boxWidth;
+		node.height = boxHeight;
+		node.centerX = boxWidth/2;
+		node.centerY = boxHeight/2;
+		
+		text.setAttribute("x", -halfWidth);
+		rect.setAttribute("x", -node.centerX);
+		rect.setAttribute("y", -node.centerY);
+		rect.setAttribute("width", node.width);
+		rect.setAttribute("height", node.height);
+		
+		nodeG.setAttribute("class", "node");
+		nodeG.jobid=node.id;
+	},
+	drawCircleNode: function(self, node, bounds) {
 		var svg = self.svgGraph;
 		var svgns = self.svgns;
 
@@ -274,36 +459,36 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var yOffset = 10;
 		
 		var nodeG = document.createElementNS(svgns, "g");
-		nodeG.setAttributeNS(null, "class", "jobnode");
-		nodeG.setAttributeNS(null, "font-family", "helvetica");
-		nodeG.setAttributeNS(null, "transform", "translate(" + node.x + "," + node.y + ")");
+		nodeG.setAttribute("class", "jobnode");
+		nodeG.setAttribute("font-family", "helvetica");
+		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
 		this.gNodes[node.id] = nodeG;
 		
 		var innerG = document.createElementNS(svgns, "g");
-		innerG.setAttributeNS(null, "transform", "translate(-10,-10)");
+		innerG.setAttribute("transform", "translate(-10,-10)");
 		
 		var circle = document.createElementNS(svgns, 'circle');
-		circle.setAttributeNS(null, "cy", 10);
-		circle.setAttributeNS(null, "cx", 10);
-		circle.setAttributeNS(null, "r", 12);
-		circle.setAttributeNS(null, "style", "width:inherit;stroke-opacity:1");
-		
+		circle.setAttribute("cy", 10);
+		circle.setAttribute("cx", 10);
+		circle.setAttribute("r", 12);
+		circle.setAttribute("style", "width:inherit;stroke-opacity:1");
+		//circle.setAttribute("class", "border");
+		//circle.setAttribute("class", "nodecontainer");
 		
 		var text = document.createElementNS(svgns, 'text');
 		var textLabel = document.createTextNode(node.label);
 		text.appendChild(textLabel);
-		text.setAttributeNS(null, "x", 4);
-		text.setAttributeNS(null, "y", 15);
-		text.setAttributeNS(null, "height", 10); 
+		text.setAttribute("x", 0);
+		text.setAttribute("y", 0);
 				
 		this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
 		
 		var backRect = document.createElementNS(svgns, 'rect');
-		backRect.setAttributeNS(null, "x", 0);
-		backRect.setAttributeNS(null, "y", 2);
-		backRect.setAttributeNS(null, "class", "backboard");
-		backRect.setAttributeNS(null, "width", 10);
-		backRect.setAttributeNS(null, "height", 15);
+		backRect.setAttribute("x", 0);
+		backRect.setAttribute("y", 2);
+		backRect.setAttribute("class", "backboard");
+		backRect.setAttribute("width", 10);
+		backRect.setAttribute("height", 15);
 		
 		innerG.appendChild(circle);
 		innerG.appendChild(backRect);
@@ -316,11 +501,11 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		// Need to get text width after attaching to SVG.
 		var computeText = text.getComputedTextLength();
 		var halfWidth = computeText/2;
-		text.setAttributeNS(null, "x", -halfWidth + 10);
-		backRect.setAttributeNS(null, "x", -halfWidth);
-		backRect.setAttributeNS(null, "width", computeText + 20);
+		text.setAttribute("x", -halfWidth + 10);
+		backRect.setAttribute("x", -halfWidth);
+		backRect.setAttribute("width", computeText + 20);
 
-		nodeG.setAttributeNS(null, "class", "node");
+		nodeG.setAttribute("class", "node");
 		nodeG.jobid=node.id;
 	},
 	addBounds: function(toBounds, addBounds) {
@@ -333,6 +518,20 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var bounds = this.graphBounds;
 		var param = {x: bounds.minX, y: bounds.minY, width: (bounds.maxX - bounds.minX), height: (bounds.maxY - bounds.minY), duration: duration };
 
-		$(this.svgGraph).svgNavigate("transformToBox", param);
+		this.panZoom(param);
+	},
+	centerNode: function(jobId) {
+		var node = this.nodes[jobId];
+		
+		var offset = 150;
+		var widthHeight = offset*2;
+		var x = node.x - offset;
+		var y = node.y - offset;
+		
+		this.panZoom({x: x, y: y, width: widthHeight, height: widthHeight});
+	},
+	panZoom: function(params) {
+		params.maxScale = 2;
+		$(this.svgGraph).svgNavigate("transformToBox", params);
 	}
 });
\ No newline at end of file
diff --git a/src/web/js/svgNavigate.js b/src/web/js/svgNavigate.js
index 68121f9..d1f7892 100644
--- a/src/web/js/svgNavigate.js
+++ b/src/web/js/svgNavigate.js
@@ -316,8 +316,19 @@
 			var aspectRatioDiv = divHeight/divWidth;
 
 			var scale = aspectRatioGraph > aspectRatioDiv ? (divHeight/height)*factor : (divWidth/width)*factor;
-			console.log("(" + x + "," + y + "," + width.toPrecision(4) + "," + height.toPrecision(4) + ")");
-			console.log("(rg:" + aspectRatioGraph.toPrecision(3) + ",rd:" + aspectRatioDiv.toPrecision(3) + "," + scale.toPrecision(3) + ")");
+			//console.log("(" + x + "," + y + "," + width.toPrecision(4) + "," + height.toPrecision(4) + ")");
+			//console.log("(rg:" + aspectRatioGraph.toPrecision(3) + ",rd:" + aspectRatioDiv.toPrecision(3) + "," + scale.toPrecision(3) + ")");
+			
+			if (arguments.maxScale) {
+				if (scale > arguments.maxScale) {
+					scale = arguments.maxScale;
+				}
+			}
+			if (arguments.minScale) {
+				if (scale < arguments.minScale) {
+					scale = arguments.minScale;
+				}
+			}
 			
 			// Center
 			var scaledWidth = width*scale;

unit/build.xml 11(+11 -0)

diff --git a/unit/build.xml b/unit/build.xml
index cb07807..64d1314 100644
--- a/unit/build.xml
+++ b/unit/build.xml
@@ -87,4 +87,15 @@
 		</zip>
 	</target>
 	
+	<target name="package-embedded" depends="jars" description="Creates a test zip">
+		<delete dir="${dist.packages.dir}" />
+		<mkdir dir="${dist.packages.dir}" />
+		
+		<!-- Tarball it -->
+		<zip destfile="${dist.packages.dir}/embedded.zip">
+			<zipfileset dir="${dist.jar.dir}" />
+			<zipfileset dir="${base.dir}/unit/executions/embedded" />
+		</zip>
+	</target>
+	
 </project>
diff --git a/unit/executions/embedded/innerFlow.job b/unit/executions/embedded/innerFlow.job
new file mode 100644
index 0000000..da71d64
--- /dev/null
+++ b/unit/executions/embedded/innerFlow.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobB,innerJobC
\ No newline at end of file
diff --git a/unit/executions/embedded/innerJobA.job b/unit/executions/embedded/innerJobA.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/unit/executions/embedded/innerJobA.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/unit/executions/embedded/innerJobB.job b/unit/executions/embedded/innerJobB.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/unit/executions/embedded/innerJobB.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/unit/executions/embedded/innerJobC.job b/unit/executions/embedded/innerJobC.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/unit/executions/embedded/innerJobC.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/unit/executions/embedded/joba.job b/unit/executions/embedded/joba.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/unit/executions/embedded/joba.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/unit/executions/embedded/jobb.job b/unit/executions/embedded/jobb.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/unit/executions/embedded/jobb.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/unit/executions/embedded/jobc.job b/unit/executions/embedded/jobc.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/unit/executions/embedded/jobc.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/unit/executions/embedded/jobd.job b/unit/executions/embedded/jobd.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/unit/executions/embedded/jobd.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/unit/executions/embedded/jobe.job b/unit/executions/embedded/jobe.job
new file mode 100644
index 0000000..fe986d5
--- /dev/null
+++ b/unit/executions/embedded/jobe.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=jobb,jobc,jobd
diff --git a/unit/java/azkaban/test/executor/SleepJavaJob.java b/unit/java/azkaban/test/executor/SleepJavaJob.java
index ac831fc..535151a 100644
--- a/unit/java/azkaban/test/executor/SleepJavaJob.java
+++ b/unit/java/azkaban/test/executor/SleepJavaJob.java
@@ -1,6 +1,9 @@
 package azkaban.test.executor;
 
+import java.io.BufferedReader;
+import java.io.FileReader;
 import java.util.Map;
+import java.util.Properties;
 
 public class SleepJavaJob {
 	private boolean fail;
@@ -8,8 +11,19 @@ public class SleepJavaJob {
 	private int attempts;
 	private int currentAttempt;
 
+	public SleepJavaJob(String id, Properties props) {
+		setup(props);
+	}
+	
 	public SleepJavaJob(String id, Map<String, String> parameters) {
-		String failStr = parameters.get("fail");
+		Properties properties = new Properties();
+		properties.putAll(parameters);
+		
+		setup(properties);
+	}
+	
+	private void setup(Properties props) {
+		String failStr = (String)props.get("fail");
 		
 		if (failStr == null || failStr.equals("false")) {
 			fail = false;
@@ -18,15 +32,15 @@ public class SleepJavaJob {
 			fail = true;
 		}
 	
-		currentAttempt = parameters.containsKey("azkaban.job.attempt") ? Integer.parseInt(parameters.get("azkaban.job.attempt")) : 0;
-		String attemptString = parameters.get("passRetry");
+		currentAttempt = props.containsKey("azkaban.job.attempt") ? Integer.parseInt((String)props.get("azkaban.job.attempt")) : 0;
+		String attemptString = (String)props.get("passRetry");
 		if (attemptString == null) {
 			attempts = -1;
 		}
 		else {
 			attempts = Integer.valueOf(attemptString);
 		}
-		seconds = parameters.get("seconds");
+		seconds = (String)props.get("seconds");
 
 		if (fail) {
 			System.out.println("Planning to fail after " + seconds + " seconds. Attempts left " + currentAttempt + " of " + attempts);
@@ -36,6 +50,17 @@ public class SleepJavaJob {
 		}
 	}
 	
+	public static void main(String[] args) throws Exception {
+		String propsFile = System.getenv("JOB_PROP_FILE");
+		Properties prop = new Properties();
+		prop.load(new BufferedReader(new FileReader(propsFile)));
+		
+		String jobName = System.getenv("JOB_NAME");
+		SleepJavaJob job = new SleepJavaJob(jobName, prop);
+		
+		job.run();
+	}
+	
 	public void run() throws Exception {
 		if (seconds == null) {
 			throw new RuntimeException("Seconds not set");