You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@crunch.apache.org by jw...@apache.org on 2013/04/23 22:41:35 UTC

[33/43] CRUNCH-196: crunch -> crunch-core rename to fix build issues

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PCollectionImpl.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PCollectionImpl.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PCollectionImpl.java
new file mode 100644
index 0000000..6ea9c4c
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PCollectionImpl.java
@@ -0,0 +1,295 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.collect;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.crunch.DoFn;
+import org.apache.crunch.FilterFn;
+import org.apache.crunch.MapFn;
+import org.apache.crunch.PCollection;
+import org.apache.crunch.PObject;
+import org.apache.crunch.PTable;
+import org.apache.crunch.Pair;
+import org.apache.crunch.ParallelDoOptions;
+import org.apache.crunch.Pipeline;
+import org.apache.crunch.SourceTarget;
+import org.apache.crunch.Target;
+import org.apache.crunch.fn.ExtractKeyFn;
+import org.apache.crunch.fn.IdentityFn;
+import org.apache.crunch.impl.mr.MRPipeline;
+import org.apache.crunch.impl.mr.plan.DoNode;
+import org.apache.crunch.lib.Aggregate;
+import org.apache.crunch.materialize.pobject.CollectionPObject;
+import org.apache.crunch.types.PTableType;
+import org.apache.crunch.types.PType;
+import org.apache.crunch.types.PTypeFamily;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+public abstract class PCollectionImpl<S> implements PCollection<S> {
+
+  private static final Log LOG = LogFactory.getLog(PCollectionImpl.class);
+
+  private final String name;
+  protected MRPipeline pipeline;
+  protected SourceTarget<S> materializedAt;
+  private final ParallelDoOptions options;
+  
+  public PCollectionImpl(String name) {
+    this(name, ParallelDoOptions.builder().build());
+  }
+  
+  public PCollectionImpl(String name, ParallelDoOptions options) {
+    this.name = name;
+    this.options = options;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public String toString() {
+    return getName();
+  }
+
+  @Override
+  public PCollection<S> union(PCollection<S> other) {
+    return union(new PCollection[] { other });
+  }
+  
+  @Override
+  public PCollection<S> union(PCollection<S>... collections) {
+    List<PCollectionImpl<S>> internal = Lists.newArrayList();
+    internal.add(this);
+    for (PCollection<S> collection : collections) {
+      internal.add((PCollectionImpl<S>) collection.parallelDo(IdentityFn.<S>getInstance(), collection.getPType()));
+    }
+    return new UnionCollection<S>(internal);
+  }
+
+  @Override
+  public <T> PCollection<T> parallelDo(DoFn<S, T> fn, PType<T> type) {
+    MRPipeline pipeline = (MRPipeline) getPipeline();
+    return parallelDo("S" + pipeline.getNextAnonymousStageId(), fn, type);
+  }
+
+  @Override
+  public <T> PCollection<T> parallelDo(String name, DoFn<S, T> fn, PType<T> type) {
+    return new DoCollectionImpl<T>(name, getChainingCollection(), fn, type);
+  }
+  
+  @Override
+  public <T> PCollection<T> parallelDo(String name, DoFn<S, T> fn, PType<T> type,
+      ParallelDoOptions options) {
+    return new DoCollectionImpl<T>(name, getChainingCollection(), fn, type, options);
+  }
+  
+  @Override
+  public <K, V> PTable<K, V> parallelDo(DoFn<S, Pair<K, V>> fn, PTableType<K, V> type) {
+    MRPipeline pipeline = (MRPipeline) getPipeline();
+    return parallelDo("S" + pipeline.getNextAnonymousStageId(), fn, type);
+  }
+
+  @Override
+  public <K, V> PTable<K, V> parallelDo(String name, DoFn<S, Pair<K, V>> fn, PTableType<K, V> type) {
+    return new DoTableImpl<K, V>(name, getChainingCollection(), fn, type);
+  }
+
+  @Override
+  public <K, V> PTable<K, V> parallelDo(String name, DoFn<S, Pair<K, V>> fn, PTableType<K, V> type,
+      ParallelDoOptions options) {
+    return new DoTableImpl<K, V>(name, getChainingCollection(), fn, type, options);
+  }
+
+  public PCollection<S> write(Target target) {
+    if (materializedAt != null) {
+      getPipeline().write(new InputCollection<S>(materializedAt, (MRPipeline) getPipeline()), target);
+    } else {
+      getPipeline().write(this, target);
+    }
+    return this;
+  }
+
+  @Override
+  public PCollection<S> write(Target target, Target.WriteMode writeMode) {
+    if (materializedAt != null) {
+      getPipeline().write(new InputCollection<S>(materializedAt, (MRPipeline) getPipeline()), target,
+          writeMode);
+    } else {
+      getPipeline().write(this, target, writeMode);
+    }
+    return this;
+  }
+  
+  @Override
+  public Iterable<S> materialize() {
+    if (getSize() == 0) {
+      LOG.warn("Materializing an empty PCollection: " + this.getName());
+      return Collections.emptyList();
+    }
+    return getPipeline().materialize(this);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public PObject<Collection<S>> asCollection() {
+    return new CollectionPObject<S>(this);
+  }
+
+  public SourceTarget<S> getMaterializedAt() {
+    return materializedAt;
+  }
+
+  public void materializeAt(SourceTarget<S> sourceTarget) {
+    this.materializedAt = sourceTarget;
+  }
+
+  @Override
+  public PCollection<S> filter(FilterFn<S> filterFn) {
+    return parallelDo(filterFn, getPType());
+  }
+
+  @Override
+  public PCollection<S> filter(String name, FilterFn<S> filterFn) {
+    return parallelDo(name, filterFn, getPType());
+  }
+
+  @Override
+  public <K> PTable<K, S> by(MapFn<S, K> mapFn, PType<K> keyType) {
+    return parallelDo(new ExtractKeyFn<K, S>(mapFn), getTypeFamily().tableOf(keyType, getPType()));
+  }
+
+  @Override
+  public <K> PTable<K, S> by(String name, MapFn<S, K> mapFn, PType<K> keyType) {
+    return parallelDo(name, new ExtractKeyFn<K, S>(mapFn), getTypeFamily().tableOf(keyType, getPType()));
+  }
+
+  @Override
+  public PTable<S, Long> count() {
+    return Aggregate.count(this);
+  }
+
+  @Override
+  public PObject<Long> length() {
+    return Aggregate.length(this);
+  }
+
+  @Override
+  public PObject<S> max() {
+    return Aggregate.max(this);
+  }
+
+  @Override
+  public PObject<S> min() {
+    return Aggregate.min(this);
+  }
+
+  @Override
+  public PTypeFamily getTypeFamily() {
+    return getPType().getFamily();
+  }
+
+  public abstract DoNode createDoNode();
+
+  public abstract List<PCollectionImpl<?>> getParents();
+
+  public PCollectionImpl<?> getOnlyParent() {
+    List<PCollectionImpl<?>> parents = getParents();
+    if (parents.size() != 1) {
+      throw new IllegalArgumentException("Expected exactly one parent PCollection");
+    }
+    return parents.get(0);
+  }
+
+  @Override
+  public Pipeline getPipeline() {
+    if (pipeline == null) {
+      pipeline = (MRPipeline) getParents().get(0).getPipeline();
+    }
+    return pipeline;
+  }
+  
+  public Set<SourceTarget<?>> getTargetDependencies() {
+    Set<SourceTarget<?>> targetDeps = options.getSourceTargets();
+    for (PCollectionImpl<?> parent : getParents()) {
+      targetDeps = Sets.union(targetDeps, parent.getTargetDependencies());
+    }
+    return targetDeps;
+  }
+  
+  public int getDepth() {
+    int parentMax = 0;
+    for (PCollectionImpl parent : getParents()) {
+      parentMax = Math.max(parent.getDepth(), parentMax);
+    }
+    return 1 + parentMax;
+  }
+
+  public interface Visitor {
+    void visitInputCollection(InputCollection<?> collection);
+
+    void visitUnionCollection(UnionCollection<?> collection);
+
+    void visitDoFnCollection(DoCollectionImpl<?> collection);
+
+    void visitDoTable(DoTableImpl<?, ?> collection);
+
+    void visitGroupedTable(PGroupedTableImpl<?, ?> collection);
+  }
+
+  public void accept(Visitor visitor) {
+    if (materializedAt != null) {
+      visitor.visitInputCollection(new InputCollection<S>(materializedAt, (MRPipeline) getPipeline()));
+    } else {
+      acceptInternal(visitor);
+    }
+  }
+
+  protected abstract void acceptInternal(Visitor visitor);
+
+  @Override
+  public long getSize() {
+    if (materializedAt != null) {
+      long sz = materializedAt.getSize(getPipeline().getConfiguration());
+      if (sz > 0) {
+        return sz;
+      }
+    }
+    return getSizeInternal();
+  }
+
+  protected abstract long getSizeInternal();
+  
+  /**
+   * Retrieve the PCollectionImpl to be used for chaining within PCollectionImpls further down the pipeline.
+   * @return The PCollectionImpl instance to be chained
+   */
+  protected PCollectionImpl<S> getChainingCollection(){
+    return this;
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PGroupedTableImpl.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PGroupedTableImpl.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PGroupedTableImpl.java
new file mode 100644
index 0000000..ccac5d5
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PGroupedTableImpl.java
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.collect;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.crunch.Aggregator;
+import org.apache.crunch.CombineFn;
+import org.apache.crunch.DoFn;
+import org.apache.crunch.Emitter;
+import org.apache.crunch.GroupingOptions;
+import org.apache.crunch.PGroupedTable;
+import org.apache.crunch.PTable;
+import org.apache.crunch.Pair;
+import org.apache.crunch.SourceTarget;
+import org.apache.crunch.fn.Aggregators;
+import org.apache.crunch.impl.mr.plan.DoNode;
+import org.apache.crunch.types.PGroupedTableType;
+import org.apache.crunch.types.PType;
+import org.apache.crunch.util.PartitionUtils;
+import org.apache.hadoop.mapreduce.Job;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+public class PGroupedTableImpl<K, V> extends PCollectionImpl<Pair<K, Iterable<V>>> implements PGroupedTable<K, V> {
+
+  private static final Log LOG = LogFactory.getLog(PGroupedTableImpl.class);
+
+  private final PTableBase<K, V> parent;
+  private final GroupingOptions groupingOptions;
+  private final PGroupedTableType<K, V> ptype;
+  
+  PGroupedTableImpl(PTableBase<K, V> parent) {
+    this(parent, null);
+  }
+
+  PGroupedTableImpl(PTableBase<K, V> parent, GroupingOptions groupingOptions) {
+    super("GBK");
+    this.parent = parent;
+    this.groupingOptions = groupingOptions;
+    this.ptype = parent.getPTableType().getGroupedTableType();
+  }
+
+  public void configureShuffle(Job job) {
+    ptype.configureShuffle(job, groupingOptions);
+    if (groupingOptions == null || groupingOptions.getNumReducers() <= 0) {
+      int numReduceTasks = PartitionUtils.getRecommendedPartitions(this, getPipeline().getConfiguration());
+      if (numReduceTasks > 0) {
+        job.setNumReduceTasks(numReduceTasks);
+        LOG.info(String.format("Setting num reduce tasks to %d", numReduceTasks));
+      } else {
+        LOG.warn("Attempted to set a negative number of reduce tasks");
+      }
+    }
+  }
+
+  @Override
+  protected long getSizeInternal() {
+    return parent.getSizeInternal();
+  }
+
+  @Override
+  public PType<Pair<K, Iterable<V>>> getPType() {
+    return ptype;
+  }
+
+  @Override
+  public PTable<K, V> combineValues(CombineFn<K, V> combineFn) {
+    return new DoTableImpl<K, V>("combine", getChainingCollection(), combineFn, parent.getPTableType());
+  }
+
+  @Override
+  public PTable<K, V> combineValues(Aggregator<V> agg) {
+    return combineValues(Aggregators.<K, V>toCombineFn(agg));
+  }
+
+  private static class Ungroup<K, V> extends DoFn<Pair<K, Iterable<V>>, Pair<K, V>> {
+    @Override
+    public void process(Pair<K, Iterable<V>> input, Emitter<Pair<K, V>> emitter) {
+      for (V v : input.second()) {
+        emitter.emit(Pair.of(input.first(), v));
+      }
+    }
+  }
+
+  public PTable<K, V> ungroup() {
+    return parallelDo("ungroup", new Ungroup<K, V>(), parent.getPTableType());
+  }
+
+  @Override
+  protected void acceptInternal(PCollectionImpl.Visitor visitor) {
+    visitor.visitGroupedTable(this);
+  }
+
+  @Override
+  public Set<SourceTarget<?>> getTargetDependencies() {
+    Set<SourceTarget<?>> td = Sets.newHashSet(super.getTargetDependencies());
+    if (groupingOptions != null) {
+      td.addAll(groupingOptions.getSourceTargets());
+    }
+    return ImmutableSet.copyOf(td);
+  }
+  
+  @Override
+  public List<PCollectionImpl<?>> getParents() {
+    return ImmutableList.<PCollectionImpl<?>> of(parent);
+  }
+
+  @Override
+  public DoNode createDoNode() {
+    return DoNode.createFnNode(getName(), ptype.getInputMapFn(), ptype);
+  }
+
+  public DoNode getGroupingNode() {
+    return DoNode.createGroupingNode("", ptype);
+  }
+  
+  @Override
+  protected PCollectionImpl<Pair<K, Iterable<V>>> getChainingCollection() {
+    // Use a copy for chaining to allow sending the output of a single grouped table to multiple outputs
+    // TODO This should be implemented in a cleaner way in the planner
+    return new PGroupedTableImpl<K, V>(parent, groupingOptions);
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PTableBase.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PTableBase.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PTableBase.java
new file mode 100644
index 0000000..3c2393d
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/PTableBase.java
@@ -0,0 +1,169 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.collect;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.crunch.FilterFn;
+import org.apache.crunch.GroupingOptions;
+import org.apache.crunch.PCollection;
+import org.apache.crunch.PObject;
+import org.apache.crunch.PTable;
+import org.apache.crunch.Pair;
+import org.apache.crunch.ParallelDoOptions;
+import org.apache.crunch.TableSource;
+import org.apache.crunch.Target;
+import org.apache.crunch.impl.mr.MRPipeline;
+import org.apache.crunch.lib.Aggregate;
+import org.apache.crunch.lib.Cogroup;
+import org.apache.crunch.lib.Join;
+import org.apache.crunch.lib.PTables;
+import org.apache.crunch.materialize.MaterializableMap;
+import org.apache.crunch.materialize.pobject.MapPObject;
+import org.apache.crunch.types.PType;
+
+import com.google.common.collect.Lists;
+
+abstract class PTableBase<K, V> extends PCollectionImpl<Pair<K, V>> implements PTable<K, V> {
+
+  public PTableBase(String name) {
+    super(name);
+  }
+
+  public PTableBase(String name, ParallelDoOptions options) {
+    super(name, options);
+  }
+  
+  public PType<K> getKeyType() {
+    return getPTableType().getKeyType();
+  }
+
+  public PType<V> getValueType() {
+    return getPTableType().getValueType();
+  }
+
+  public PGroupedTableImpl<K, V> groupByKey() {
+    return new PGroupedTableImpl<K, V>(this);
+  }
+
+  public PGroupedTableImpl<K, V> groupByKey(int numReduceTasks) {
+    return new PGroupedTableImpl<K, V>(this, GroupingOptions.builder().numReducers(numReduceTasks).build());
+  }
+
+  public PGroupedTableImpl<K, V> groupByKey(GroupingOptions groupingOptions) {
+    return new PGroupedTableImpl<K, V>(this, groupingOptions);
+  }
+
+  @Override
+  public PTable<K, V> union(PTable<K, V> other) {
+    return union(new PTable[] { other });
+  }
+  
+  @Override
+  public PTable<K, V> union(PTable<K, V>... others) {
+    List<PTableBase<K, V>> internal = Lists.newArrayList();
+    internal.add(this);
+    for (PTable<K, V> table : others) {
+      internal.add((PTableBase<K, V>) table);
+    }
+    return new UnionTable<K, V>(internal);
+  }
+
+  @Override
+  public PTable<K, V> write(Target target) {
+    if (getMaterializedAt() != null) {
+      getPipeline().write(new InputTable<K, V>(
+          (TableSource<K, V>) getMaterializedAt(), (MRPipeline) getPipeline()), target);
+    } else {
+      getPipeline().write(this, target);
+    }
+    return this;
+  }
+
+  @Override
+  public PTable<K, V> write(Target target, Target.WriteMode writeMode) {
+    if (getMaterializedAt() != null) {
+      getPipeline().write(new InputTable<K, V>(
+          (TableSource<K, V>) getMaterializedAt(), (MRPipeline) getPipeline()), target, writeMode);
+    } else {
+      getPipeline().write(this, target, writeMode);
+    }
+    return this;
+  }
+  
+  @Override
+  public PTable<K, V> filter(FilterFn<Pair<K, V>> filterFn) {
+    return parallelDo(filterFn, getPTableType());
+  }
+  
+  @Override
+  public PTable<K, V> filter(String name, FilterFn<Pair<K, V>> filterFn) {
+    return parallelDo(name, filterFn, getPTableType());
+  }
+  
+  @Override
+  public PTable<K, V> top(int count) {
+    return Aggregate.top(this, count, true);
+  }
+
+  @Override
+  public PTable<K, V> bottom(int count) {
+    return Aggregate.top(this, count, false);
+  }
+
+  @Override
+  public PTable<K, Collection<V>> collectValues() {
+    return Aggregate.collectValues(this);
+  }
+
+  @Override
+  public <U> PTable<K, Pair<V, U>> join(PTable<K, U> other) {
+    return Join.join(this, other);
+  }
+
+  @Override
+  public <U> PTable<K, Pair<Collection<V>, Collection<U>>> cogroup(PTable<K, U> other) {
+    return Cogroup.cogroup(this, other);
+  }
+
+  @Override
+  public PCollection<K> keys() {
+    return PTables.keys(this);
+  }
+
+  @Override
+  public PCollection<V> values() {
+    return PTables.values(this);
+  }
+
+  /**
+   * Returns a Map<K, V> made up of the keys and values in this PTable.
+   */
+  @Override
+  public Map<K, V> materializeToMap() {
+    return new MaterializableMap<K, V>(this.materialize());
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public PObject<Map<K, V>> asMap() {
+    return new MapPObject<K, V>(this);
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionCollection.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionCollection.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionCollection.java
new file mode 100644
index 0000000..7b3dd7b
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionCollection.java
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.collect;
+
+import java.util.List;
+
+import org.apache.crunch.impl.mr.MRPipeline;
+import org.apache.crunch.impl.mr.plan.DoNode;
+import org.apache.crunch.types.PType;
+
+import com.google.common.collect.ImmutableList;
+
+public class UnionCollection<S> extends PCollectionImpl<S> {
+
+  private List<PCollectionImpl<S>> parents;
+  private long size = 0;
+
+  private static <S> String flatName(List<PCollectionImpl<S>> collections) {
+    StringBuilder sb = new StringBuilder("union(");
+    for (int i = 0; i < collections.size(); i++) {
+      if (i != 0) {
+        sb.append(',');
+      }
+      sb.append(collections.get(i).getName());
+    }
+    return sb.append(')').toString();
+  }
+
+  UnionCollection(List<PCollectionImpl<S>> collections) {
+    super(flatName(collections));
+    this.parents = ImmutableList.copyOf(collections);
+    this.pipeline = (MRPipeline) parents.get(0).getPipeline();
+    for (PCollectionImpl<S> parent : parents) {
+      if (this.pipeline != parent.getPipeline()) {
+        throw new IllegalStateException("Cannot union PCollections from different Pipeline instances");
+      }
+      size += parent.getSize();
+    }
+  }
+
+  @Override
+  protected long getSizeInternal() {
+    return size;
+  }
+
+  @Override
+  protected void acceptInternal(PCollectionImpl.Visitor visitor) {
+    visitor.visitUnionCollection(this);
+  }
+
+  @Override
+  public PType<S> getPType() {
+    return parents.get(0).getPType();
+  }
+
+  @Override
+  public List<PCollectionImpl<?>> getParents() {
+    return ImmutableList.<PCollectionImpl<?>> copyOf(parents);
+  }
+
+  @Override
+  public DoNode createDoNode() {
+    throw new UnsupportedOperationException("Unioned collection does not support DoNodes");
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionTable.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionTable.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionTable.java
new file mode 100644
index 0000000..a369432
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/collect/UnionTable.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.collect;
+
+import java.util.List;
+
+import org.apache.crunch.Pair;
+import org.apache.crunch.impl.mr.MRPipeline;
+import org.apache.crunch.impl.mr.plan.DoNode;
+import org.apache.crunch.types.PTableType;
+import org.apache.crunch.types.PType;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class UnionTable<K, V> extends PTableBase<K, V> {
+
+  private PTableType<K, V> ptype;
+  private List<PCollectionImpl<Pair<K, V>>> parents;
+  private long size;
+
+  private static <K, V> String flatName(List<PTableBase<K, V>> tables) {
+    StringBuilder sb = new StringBuilder("union(");
+    for (int i = 0; i < tables.size(); i++) {
+      if (i != 0) {
+        sb.append(',');
+      }
+      sb.append(tables.get(i).getName());
+    }
+    return sb.append(')').toString();
+  }
+
+  public UnionTable(List<PTableBase<K, V>> tables) {
+    super(flatName(tables));
+    this.ptype = tables.get(0).getPTableType();
+    this.pipeline = (MRPipeline) tables.get(0).getPipeline();
+    this.parents = Lists.newArrayList();
+    for (PTableBase<K, V> parent : tables) {
+      if (pipeline != parent.getPipeline()) {
+        throw new IllegalStateException("Cannot union PTables from different Pipeline instances");
+      }
+      this.parents.add(parent);
+      size += parent.getSize();
+    }
+  }
+
+  @Override
+  protected long getSizeInternal() {
+    return size;
+  }
+
+  @Override
+  public PTableType<K, V> getPTableType() {
+    return ptype;
+  }
+
+  @Override
+  public PType<Pair<K, V>> getPType() {
+    return ptype;
+  }
+
+  @Override
+  public List<PCollectionImpl<?>> getParents() {
+    return ImmutableList.<PCollectionImpl<?>> copyOf(parents);
+  }
+
+  @Override
+  protected void acceptInternal(PCollectionImpl.Visitor visitor) {
+    visitor.visitUnionCollection(new UnionCollection<Pair<K, V>>(parents));
+  }
+
+  @Override
+  public DoNode createDoNode() {
+    throw new UnsupportedOperationException("Unioned table does not support do nodes");
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/IntermediateEmitter.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/IntermediateEmitter.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/IntermediateEmitter.java
new file mode 100644
index 0000000..b6df98b
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/IntermediateEmitter.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.emit;
+
+import java.util.List;
+
+import org.apache.crunch.DoFn;
+import org.apache.crunch.Emitter;
+import org.apache.crunch.impl.mr.run.RTNode;
+import org.apache.crunch.types.PType;
+import org.apache.hadoop.conf.Configuration;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * An {@link Emitter} implementation that links the output of one {@link DoFn} to the input of
+ * another {@code DoFn}.
+ * 
+ */
+public class IntermediateEmitter implements Emitter<Object> {
+
+  private final List<RTNode> children;
+  private final Configuration conf;
+  private final PType<Object> outputPType;
+  private final boolean needDetachedValues;
+
+  public IntermediateEmitter(PType<Object> outputPType, List<RTNode> children, Configuration conf) {
+    this.outputPType = outputPType;
+    this.children = ImmutableList.copyOf(children);
+    this.conf = conf;
+
+    outputPType.initialize(conf);
+    needDetachedValues = this.children.size() > 1;
+  }
+
+  public void emit(Object emitted) {
+    for (RTNode child : children) {
+      Object value = emitted;
+      if (needDetachedValues) {
+        value = this.outputPType.getDetachedValue(emitted);
+      }
+      child.process(value);
+    }
+  }
+
+  public void flush() {
+    // No-op
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/MultipleOutputEmitter.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/MultipleOutputEmitter.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/MultipleOutputEmitter.java
new file mode 100644
index 0000000..2e58fed
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/MultipleOutputEmitter.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.emit;
+
+import java.io.IOException;
+
+import org.apache.crunch.CrunchRuntimeException;
+import org.apache.crunch.Emitter;
+import org.apache.crunch.io.CrunchOutputs;
+import org.apache.crunch.types.Converter;
+
+public class MultipleOutputEmitter<T, K, V> implements Emitter<T> {
+
+  private final Converter converter;
+  private final CrunchOutputs<K, V> outputs;
+  private final String outputName;
+
+  public MultipleOutputEmitter(Converter converter, CrunchOutputs<K, V> outputs,
+      String outputName) {
+    this.converter = converter;
+    this.outputs = outputs;
+    this.outputName = outputName;
+  }
+
+  @Override
+  public void emit(T emitted) {
+    try {
+      this.outputs.write(outputName,
+          (K) converter.outputKey(emitted),
+          (V) converter.outputValue(emitted));
+    } catch (Exception e) {
+      throw new CrunchRuntimeException(e);
+    }
+  }
+
+  @Override
+  public void flush() {
+    // No-op
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/OutputEmitter.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/OutputEmitter.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/OutputEmitter.java
new file mode 100644
index 0000000..bc3ae0d
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/emit/OutputEmitter.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.emit;
+
+import java.io.IOException;
+
+import org.apache.crunch.CrunchRuntimeException;
+import org.apache.crunch.Emitter;
+import org.apache.crunch.types.Converter;
+import org.apache.hadoop.mapreduce.TaskInputOutputContext;
+
+public class OutputEmitter<T, K, V> implements Emitter<T> {
+
+  private final Converter<K, V, Object, Object> converter;
+  private final TaskInputOutputContext<?, ?, K, V> context;
+
+  public OutputEmitter(Converter<K, V, Object, Object> converter, TaskInputOutputContext<?, ?, K, V> context) {
+    this.converter = converter;
+    this.context = context;
+  }
+
+  public void emit(T emitted) {
+    try {
+      K key = converter.outputKey(emitted);
+      V value = converter.outputValue(emitted);
+      this.context.write(key, value);
+    } catch (IOException e) {
+      throw new CrunchRuntimeException(e);
+    } catch (InterruptedException e) {
+      throw new CrunchRuntimeException(e);
+    }
+  }
+
+  public void flush() {
+    // No-op
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CappedExponentialCounter.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CappedExponentialCounter.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CappedExponentialCounter.java
new file mode 100644
index 0000000..d90f2e8
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CappedExponentialCounter.java
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.exec;
+
+/**
+ * Generate a series of capped numbers exponentially.
+ *
+ * It is used for creating retry intervals. It is NOT thread-safe.
+ */
+public class CappedExponentialCounter {
+
+  private long current;
+  private final long limit;
+
+  public CappedExponentialCounter(long start, long limit) {
+    this.current = start;
+    this.limit = limit;
+  }
+
+  public long get() {
+    long result = current;
+    current = Math.min(current * 2, limit);
+    return result;
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CrunchJobHooks.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CrunchJobHooks.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CrunchJobHooks.java
new file mode 100644
index 0000000..74bc9ac
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/CrunchJobHooks.java
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.exec;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.crunch.hadoop.mapreduce.lib.jobcontrol.CrunchControlledJob;
+import org.apache.crunch.impl.mr.plan.PlanningParameters;
+import org.apache.crunch.impl.mr.run.RuntimeParameters;
+import org.apache.crunch.io.FileNamingScheme;
+import org.apache.crunch.io.PathTarget;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.FileUtil;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.mapreduce.Job;
+import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
+
+public final class CrunchJobHooks {
+
+  private CrunchJobHooks() {}
+
+  /** Creates missing input directories before job is submitted. */
+  public static final class PrepareHook implements CrunchControlledJob.Hook {
+    private final Job job;
+
+    public PrepareHook(Job job) {
+      this.job = job;
+    }
+
+    @Override
+    public void run() throws IOException {
+      Configuration conf = job.getConfiguration();
+      if (conf.getBoolean(RuntimeParameters.CREATE_DIR, false)) {
+        Path[] inputPaths = FileInputFormat.getInputPaths(job);
+        for (Path inputPath : inputPaths) {
+          FileSystem fs = inputPath.getFileSystem(conf);
+          if (!fs.exists(inputPath)) {
+            try {
+              fs.mkdirs(inputPath);
+            } catch (IOException e) {
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /** Moving output files produced by the MapReduce job to specified directories. */
+  public static final class CompletionHook implements CrunchControlledJob.Hook {
+    private final Job job;
+    private final Path workingPath;
+    private final Map<Integer, PathTarget> multiPaths;
+    private final boolean mapOnlyJob;
+
+    public CompletionHook(Job job, Path workingPath, Map<Integer, PathTarget> multiPaths, boolean mapOnlyJob) {
+      this.job = job;
+      this.workingPath = workingPath;
+      this.multiPaths = multiPaths;
+      this.mapOnlyJob = mapOnlyJob;
+    }
+
+    @Override
+    public void run() throws IOException {
+      handleMultiPaths();
+    }
+
+    private synchronized void handleMultiPaths() throws IOException {
+      if (!multiPaths.isEmpty()) {
+        // Need to handle moving the data from the output directory of the
+        // job to the output locations specified in the paths.
+        FileSystem srcFs = workingPath.getFileSystem(job.getConfiguration());
+        for (Map.Entry<Integer, PathTarget> entry : multiPaths.entrySet()) {
+          final int i = entry.getKey();
+          final Path dst = entry.getValue().getPath();
+          FileNamingScheme fileNamingScheme = entry.getValue().getFileNamingScheme();
+
+          Path src = new Path(workingPath, PlanningParameters.MULTI_OUTPUT_PREFIX + i + "-*");
+          Path[] srcs = FileUtil.stat2Paths(srcFs.globStatus(src), src);
+          Configuration conf = job.getConfiguration();
+          FileSystem dstFs = dst.getFileSystem(conf);
+          if (!dstFs.exists(dst)) {
+            dstFs.mkdirs(dst);
+          }
+          boolean sameFs = isCompatible(srcFs, dst);
+          for (Path s : srcs) {
+            Path d = getDestFile(conf, s, dst, fileNamingScheme);
+            if (sameFs) {
+              srcFs.rename(s, d);
+            } else {
+              FileUtil.copy(srcFs, s, dstFs, d, true, true, job.getConfiguration());
+            }
+          }
+        }
+      }
+    }
+
+    private boolean isCompatible(FileSystem fs, Path path) {
+      try {
+        fs.makeQualified(path);
+        return true;
+      } catch (IllegalArgumentException e) {
+        return false;
+      }
+    }
+    private Path getDestFile(Configuration conf, Path src, Path dir, FileNamingScheme fileNamingScheme)
+        throws IOException {
+      String outputFilename = null;
+      if (mapOnlyJob) {
+        outputFilename = fileNamingScheme.getMapOutputName(conf, dir);
+      } else {
+        outputFilename = fileNamingScheme.getReduceOutputName(conf, dir, extractPartitionNumber(src.getName()));
+      }
+      if (src.getName().endsWith(org.apache.avro.mapred.AvroOutputFormat.EXT)) {
+        outputFilename += org.apache.avro.mapred.AvroOutputFormat.EXT;
+      }
+      return new Path(dir, outputFilename);
+    }
+  }
+
+  /**
+   * Extract the partition number from a raw reducer output filename.
+   *
+   * @param reduceOutputFileName The raw reducer output file name
+   * @return The partition number encoded in the filename
+   */
+  static int extractPartitionNumber(String reduceOutputFileName) {
+    Matcher matcher = Pattern.compile(".*-r-(\\d{5})").matcher(reduceOutputFileName);
+    if (matcher.find()) {
+      return Integer.parseInt(matcher.group(1), 10);
+    } else {
+      throw new IllegalArgumentException("Reducer output name '" + reduceOutputFileName + "' cannot be parsed");
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/MRExecutor.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/MRExecutor.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/MRExecutor.java
new file mode 100644
index 0000000..4c7b7ea
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/exec/MRExecutor.java
@@ -0,0 +1,198 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.exec;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.crunch.PipelineExecution;
+import org.apache.crunch.PipelineResult;
+import org.apache.crunch.SourceTarget;
+import org.apache.crunch.Target;
+import org.apache.crunch.hadoop.mapreduce.lib.jobcontrol.CrunchControlledJob;
+import org.apache.crunch.hadoop.mapreduce.lib.jobcontrol.CrunchJobControl;
+import org.apache.crunch.impl.mr.collect.PCollectionImpl;
+import org.apache.crunch.materialize.MaterializableIterable;
+import org.apache.hadoop.conf.Configuration;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Provides APIs for job control at runtime to clients.
+ *
+ * This class has a thread that submits jobs when they become ready, monitors
+ * the states of the running jobs, and updates the states of jobs based on the
+ * state changes of their depending jobs states.
+ *
+ * It is thread-safe.
+ */
+public class MRExecutor implements PipelineExecution {
+
+  private static final Log LOG = LogFactory.getLog(MRExecutor.class);
+
+  private final CrunchJobControl control;
+  private final Map<PCollectionImpl<?>, Set<Target>> outputTargets;
+  private final Map<PCollectionImpl<?>, MaterializableIterable> toMaterialize;
+  private final CountDownLatch doneSignal = new CountDownLatch(1);
+  private final CountDownLatch killSignal = new CountDownLatch(1);
+  private final CappedExponentialCounter pollInterval;
+  private AtomicReference<Status> status = new AtomicReference<Status>(Status.READY);
+  private PipelineResult result;
+  private Thread monitorThread;
+
+  private String planDotFile;
+  
+  public MRExecutor(Class<?> jarClass, Map<PCollectionImpl<?>, Set<Target>> outputTargets,
+      Map<PCollectionImpl<?>, MaterializableIterable> toMaterialize) {
+    this.control = new CrunchJobControl(jarClass.toString());
+    this.outputTargets = outputTargets;
+    this.toMaterialize = toMaterialize;
+    this.monitorThread = new Thread(new Runnable() {
+      @Override
+      public void run() {
+        monitorLoop();
+      }
+    });
+    this.pollInterval = isLocalMode()
+      ? new CappedExponentialCounter(50, 1000)
+      : new CappedExponentialCounter(500, 10000);
+  }
+
+  public void addJob(CrunchControlledJob job) {
+    this.control.addJob(job);
+  }
+
+  public void setPlanDotFile(String planDotFile) {
+    this.planDotFile = planDotFile;
+  }
+  
+  public PipelineExecution execute() {
+    monitorThread.start();
+    return this;
+  }
+
+  /** Monitors running status. It is called in {@code MonitorThread}. */
+  private void monitorLoop() {
+    try {
+      while (killSignal.getCount() > 0 && !control.allFinished()) {
+        control.pollJobStatusAndStartNewOnes();
+        killSignal.await(pollInterval.get(), TimeUnit.MILLISECONDS);
+      }
+      control.killAllRunningJobs();
+
+      List<CrunchControlledJob> failures = control.getFailedJobList();
+      if (!failures.isEmpty()) {
+        System.err.println(failures.size() + " job failure(s) occurred:");
+        for (CrunchControlledJob job : failures) {
+          System.err.println(job.getJobName() + "(" + job.getJobID() + "): " + job.getMessage());
+        }
+      }
+      List<PipelineResult.StageResult> stages = Lists.newArrayList();
+      for (CrunchControlledJob job : control.getSuccessfulJobList()) {
+        stages.add(new PipelineResult.StageResult(job.getJobName(), job.getJob().getCounters()));
+      }
+
+      for (PCollectionImpl<?> c : outputTargets.keySet()) {
+        if (toMaterialize.containsKey(c)) {
+          MaterializableIterable iter = toMaterialize.get(c);
+          if (iter.isSourceTarget()) {
+            iter.materialize();
+            c.materializeAt((SourceTarget) iter.getSource());
+          }
+        } else {
+          boolean materialized = false;
+          for (Target t : outputTargets.get(c)) {
+            if (!materialized) {
+              if (t instanceof SourceTarget) {
+                c.materializeAt((SourceTarget) t);
+                materialized = true;
+              } else {
+                SourceTarget st = t.asSourceTarget(c.getPType());
+                if (st != null) {
+                  c.materializeAt(st);
+                  materialized = true;
+                }
+              }
+            }
+          }
+        }
+      }
+
+      synchronized (this) {
+        result = new PipelineResult(stages);
+        if (killSignal.getCount() == 0) {
+          status.set(Status.KILLED);
+        } else {
+          status.set(result.succeeded() ? Status.SUCCEEDED : Status.FAILED);
+        }
+      }
+    } catch (InterruptedException e) {
+      throw new AssertionError(e); // Nobody should interrupt us.
+    } catch (IOException e) {
+      LOG.error("Pipeline failed due to exception", e);
+      status.set(Status.FAILED);
+    } finally {
+      doneSignal.countDown();
+    }
+  }
+
+  @Override
+  public String getPlanDotFile() {
+    return planDotFile;
+  }
+
+  @Override
+  public void waitFor(long timeout, TimeUnit timeUnit) throws InterruptedException {
+    doneSignal.await(timeout, timeUnit);
+  }
+
+  @Override
+  public void waitUntilDone() throws InterruptedException {
+    doneSignal.await();
+  }
+
+  @Override
+  public synchronized Status getStatus() {
+    return status.get();
+  }
+
+  @Override
+  public synchronized PipelineResult getResult() {
+    return result;
+  }
+
+  @Override
+  public void kill() throws InterruptedException {
+    killSignal.countDown();
+  }
+
+  private static boolean isLocalMode() {
+    Configuration conf = new Configuration();
+    // Try to handle MapReduce version 0.20 or 0.22
+    String jobTrackerAddress = conf.get("mapreduce.jobtracker.address",
+        conf.get("mapred.job.tracker", "local"));
+    return "local".equals(jobTrackerAddress);
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/package-info.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/package-info.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/package-info.java
new file mode 100644
index 0000000..7e403c3
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/package-info.java
@@ -0,0 +1,22 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A Pipeline implementation that runs on Hadoop MapReduce.
+ */
+package org.apache.crunch.impl.mr;

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DoNode.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DoNode.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DoNode.java
new file mode 100644
index 0000000..865369c
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DoNode.java
@@ -0,0 +1,163 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import java.util.List;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.crunch.DoFn;
+import org.apache.crunch.Source;
+import org.apache.crunch.impl.mr.run.NodeContext;
+import org.apache.crunch.impl.mr.run.RTNode;
+import org.apache.crunch.types.Converter;
+import org.apache.crunch.types.PGroupedTableType;
+import org.apache.crunch.types.PType;
+import org.apache.hadoop.conf.Configuration;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class DoNode {
+
+  private static final List<DoNode> NO_CHILDREN = ImmutableList.of();
+
+  private final DoFn fn;
+  private final String name;
+  private final PType<?> ptype;
+  private final List<DoNode> children;
+  private final Converter outputConverter;
+  private final Source<?> source;
+  private String outputName;
+
+  private DoNode(DoFn fn, String name, PType<?> ptype, List<DoNode> children, Converter outputConverter,
+      Source<?> source) {
+    this.fn = fn;
+    this.name = name;
+    this.ptype = ptype;
+    this.children = children;
+    this.outputConverter = outputConverter;
+    this.source = source;
+  }
+
+  private static List<DoNode> allowsChildren() {
+    return Lists.newArrayList();
+  }
+
+  public static <K, V> DoNode createGroupingNode(String name, PGroupedTableType<K, V> ptype) {
+    DoFn<?, ?> fn = ptype.getOutputMapFn();
+    return new DoNode(fn, name, ptype, NO_CHILDREN, ptype.getGroupingConverter(), null);
+  }
+
+  public static <S> DoNode createOutputNode(String name, PType<S> ptype) {
+    Converter outputConverter = ptype.getConverter();
+    DoFn<?, ?> fn = ptype.getOutputMapFn();
+    return new DoNode(fn, name, ptype, NO_CHILDREN, outputConverter, null);
+  }
+
+  public static DoNode createFnNode(String name, DoFn<?, ?> function, PType<?> ptype) {
+    return new DoNode(function, name, ptype, allowsChildren(), null, null);
+  }
+
+  public static <S> DoNode createInputNode(Source<S> source) {
+    PType<?> ptype = source.getType();
+    DoFn<?, ?> fn = ptype.getInputMapFn();
+    return new DoNode(fn, source.toString(), ptype, allowsChildren(), null, source);
+  }
+
+  public boolean isInputNode() {
+    return source != null;
+  }
+
+  public boolean isOutputNode() {
+    return outputConverter != null;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public List<DoNode> getChildren() {
+    return children;
+  }
+
+  public Source<?> getSource() {
+    return source;
+  }
+
+  public PType<?> getPType() {
+    return ptype;
+  }
+
+  public DoNode addChild(DoNode node) {
+    // TODO: This is sort of terrible, refactor the code to make this make more sense.
+    boolean exists = false;
+    for (DoNode child : children) {
+      if (node == child) {
+        exists = true;
+        break;
+      }
+    }
+    if (!exists) {
+      children.add(node);
+    }
+    return this;
+  }
+
+  public void setOutputName(String outputName) {
+    if (outputConverter == null) {
+      throw new IllegalStateException("Cannot set output name w/o output converter: " + outputName);
+    }
+    this.outputName = outputName;
+  }
+
+  public RTNode toRTNode(boolean inputNode, Configuration conf, NodeContext nodeContext) {
+    List<RTNode> childRTNodes = Lists.newArrayList();
+    fn.configure(conf);
+    for (DoNode child : children) {
+      childRTNodes.add(child.toRTNode(false, conf, nodeContext));
+    }
+
+    Converter inputConverter = null;
+    if (inputNode) {
+      if (nodeContext == NodeContext.MAP) {
+        inputConverter = ptype.getConverter();
+      } else {
+        inputConverter = ((PGroupedTableType<?, ?>) ptype).getGroupingConverter();
+      }
+    }
+    return new RTNode(fn, (PType<Object>) getPType(), name, childRTNodes, inputConverter, outputConverter, outputName);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null || !(other instanceof DoNode)) {
+      return false;
+    }
+    if (this == other) {
+      return true;
+    }
+    DoNode o = (DoNode) other;
+    return (name.equals(o.name) && fn.equals(o.fn) && source == o.source && outputConverter == o.outputConverter);
+  }
+
+  @Override
+  public int hashCode() {
+    HashCodeBuilder hcb = new HashCodeBuilder();
+    return hcb.append(name).append(fn).append(source).append(outputConverter).toHashCode();
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DotfileWriter.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DotfileWriter.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DotfileWriter.java
new file mode 100644
index 0000000..46d8c53
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/DotfileWriter.java
@@ -0,0 +1,238 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.crunch.Pair;
+import org.apache.crunch.Target;
+import org.apache.crunch.impl.mr.collect.InputCollection;
+import org.apache.crunch.impl.mr.collect.PCollectionImpl;
+import org.apache.crunch.impl.mr.collect.PGroupedTableImpl;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ * Writes <a href="http://www.graphviz.org">Graphviz</a> dot files to illustrate
+ * the topology of Crunch pipelines.
+ */
+public class DotfileWriter {
+  
+  /** The types of tasks within a MapReduce job. */
+  enum MRTaskType { MAP, REDUCE };
+
+  private Set<JobPrototype> jobPrototypes = Sets.newHashSet();
+  private HashMultimap<Pair<JobPrototype, MRTaskType>, String> jobNodeDeclarations = HashMultimap.create();
+  private Set<String> globalNodeDeclarations = Sets.newHashSet();
+  private Set<String> nodePathChains = Sets.newHashSet();
+
+  /**
+   * Format the declaration of a node based on a PCollection.
+   * 
+   * @param pcollectionImpl PCollection for which a node will be declared
+   * @param jobPrototype The job containing the PCollection
+   * @return The node declaration
+   */
+  String formatPCollectionNodeDeclaration(PCollectionImpl<?> pcollectionImpl, JobPrototype jobPrototype) {
+    String shape = "box";
+    if (pcollectionImpl instanceof InputCollection) {
+      shape = "folder";
+    }
+    return String.format("%s [label=\"%s\" shape=%s];", formatPCollection(pcollectionImpl, jobPrototype), pcollectionImpl.getName(),
+        shape);
+  }
+
+  /**
+   * Format a Target as a node declaration.
+   * 
+   * @param target A Target used within a MapReduce pipeline
+   * @return The global node declaration for the Target
+   */
+  String formatTargetNodeDeclaration(Target target) {
+    return String.format("\"%s\" [label=\"%s\" shape=folder];", target.toString(), target.toString());
+  }
+
+  /**
+   * Format a PCollectionImpl into a format to be used for dot files.
+   * 
+   * @param pcollectionImpl The PCollectionImpl to be formatted
+   * @param jobPrototype The job containing the PCollection
+   * @return The dot file formatted representation of the PCollectionImpl
+   */
+  String formatPCollection(PCollectionImpl<?> pcollectionImpl, JobPrototype jobPrototype) {
+    if (pcollectionImpl instanceof InputCollection) {
+      InputCollection<?> inputCollection = (InputCollection<?>) pcollectionImpl;
+      return String.format("\"%s\"", inputCollection.getSource());
+    }
+    return String.format("\"%s@%d@%d\"", pcollectionImpl.getName(), pcollectionImpl.hashCode(), jobPrototype.hashCode());
+  }
+
+  /**
+   * Format a collection of node strings into dot file syntax.
+   * 
+   * @param nodeCollection Collection of chained node strings
+   * @return The dot-formatted chain of nodes
+   */
+  String formatNodeCollection(List<String> nodeCollection) {
+    return String.format("%s;", Joiner.on(" -> ").join(nodeCollection));
+  }
+
+  /**
+   * Format a NodePath in dot file syntax.
+   * 
+   * @param nodePath The node path to be formatted
+   * @param jobPrototype The job containing the NodePath
+   * @return The dot file representation of the node path
+   */
+  List<String> formatNodePath(NodePath nodePath, JobPrototype jobPrototype) {
+    List<String> formattedNodePaths = Lists.newArrayList();
+    
+    List<PCollectionImpl<?>> pcollections = Lists.newArrayList(nodePath);
+    for (int collectionIndex = 1; collectionIndex < pcollections.size(); collectionIndex++){
+      String fromNode = formatPCollection(pcollections.get(collectionIndex - 1), jobPrototype);
+      String toNode = formatPCollection(pcollections.get(collectionIndex), jobPrototype);
+      formattedNodePaths.add(formatNodeCollection(Lists.newArrayList(fromNode, toNode)));
+    }
+    return formattedNodePaths;
+  }
+
+  /**
+   * Add a NodePath to be formatted as a list of node declarations within a
+   * single job.
+   * 
+   * @param jobPrototype The job containing the node path
+   * @param nodePath The node path to be formatted
+   */
+  void addNodePathDeclarations(JobPrototype jobPrototype, NodePath nodePath) {
+    boolean groupingEncountered = false;
+    for (PCollectionImpl<?> pcollectionImpl : nodePath) {
+      if (pcollectionImpl instanceof InputCollection) {
+        globalNodeDeclarations.add(formatPCollectionNodeDeclaration(pcollectionImpl, jobPrototype));
+      } else {
+        if (!groupingEncountered){
+          groupingEncountered = (pcollectionImpl instanceof PGroupedTableImpl);
+        }
+
+        MRTaskType taskType = groupingEncountered ? MRTaskType.REDUCE : MRTaskType.MAP;
+        jobNodeDeclarations.put(Pair.of(jobPrototype, taskType), formatPCollectionNodeDeclaration(pcollectionImpl, jobPrototype));
+      }
+    }
+  }
+
+  /**
+   * Add the chaining of a NodePath to the graph.
+   * 
+   * @param nodePath The path to be formatted as a node chain in the dot file
+   * @param jobPrototype The job containing the NodePath
+   */
+  void addNodePathChain(NodePath nodePath, JobPrototype jobPrototype) {
+    for (String nodePathChain : formatNodePath(nodePath, jobPrototype)){
+      this.nodePathChains.add(nodePathChain);
+    }
+  }
+
+  /**
+   * Get the graph attributes for a task-specific subgraph.
+   * 
+   * @param taskType The type of task in the subgraph
+   * @return Graph attributes
+   */
+  String getTaskGraphAttributes(MRTaskType taskType) {
+    if (taskType == MRTaskType.MAP) {
+      return "label = Map; color = blue;";
+    } else {
+      return "label = Reduce; color = red;";
+    }
+  }
+
+  /**
+   * Add the contents of a {@link JobPrototype} to the graph describing a
+   * pipeline.
+   * 
+   * @param jobPrototype A JobPrototype representing a portion of a MapReduce
+   *          pipeline
+   */
+  public void addJobPrototype(JobPrototype jobPrototype) {
+    jobPrototypes.add(jobPrototype);
+    if (!jobPrototype.isMapOnly()) {
+      for (NodePath nodePath : jobPrototype.getMapNodePaths()) {
+        addNodePathDeclarations(jobPrototype, nodePath);
+        addNodePathChain(nodePath, jobPrototype);
+      }
+    }
+
+    HashMultimap<Target, NodePath> targetsToNodePaths = jobPrototype.getTargetsToNodePaths();
+    for (Target target : targetsToNodePaths.keySet()) {
+      globalNodeDeclarations.add(formatTargetNodeDeclaration(target));
+      for (NodePath nodePath : targetsToNodePaths.get(target)) {
+        addNodePathDeclarations(jobPrototype, nodePath);
+        addNodePathChain(nodePath, jobPrototype);
+        nodePathChains.add(formatNodeCollection(Lists.newArrayList(formatPCollection(nodePath.descendingIterator()
+            .next(), jobPrototype), String.format("\"%s\"", target.toString()))));
+      }
+    }
+  }
+
+  /**
+   * Build up the full dot file containing the description of a MapReduce
+   * pipeline.
+   * 
+   * @return Graphviz dot file contents
+   */
+  public String buildDotfile() {
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder.append("digraph G {\n");
+    int clusterIndex = 0;
+
+    for (String globalDeclaration : globalNodeDeclarations) {
+      stringBuilder.append(String.format("  %s\n", globalDeclaration));
+    }
+
+    for (JobPrototype jobPrototype : jobPrototypes){
+      StringBuilder jobProtoStringBuilder = new StringBuilder();
+      jobProtoStringBuilder.append(String.format("  subgraph cluster%d {\n", clusterIndex++));
+      for (MRTaskType taskType : MRTaskType.values()){
+        Pair<JobPrototype,MRTaskType> jobTaskKey = Pair.of(jobPrototype, taskType);
+        if (jobNodeDeclarations.containsKey(jobTaskKey)){
+          jobProtoStringBuilder.append(String.format("    subgraph cluster%d {\n", clusterIndex++));
+          jobProtoStringBuilder.append(String.format("      %s\n", getTaskGraphAttributes(taskType)));
+          for (String declarationEntry : jobNodeDeclarations.get(jobTaskKey)){
+            jobProtoStringBuilder.append(String.format("      %s\n", declarationEntry));
+          }
+          jobProtoStringBuilder.append("    }\n");
+        }
+      }
+      jobProtoStringBuilder.append("  }\n");
+      stringBuilder.append(jobProtoStringBuilder.toString());
+    }
+    
+    for (String nodePathChain : nodePathChains) {
+      stringBuilder.append(String.format("  %s\n", nodePathChain));
+    }
+
+    stringBuilder.append("}\n");
+    return stringBuilder.toString();
+  }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Edge.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Edge.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Edge.java
new file mode 100644
index 0000000..1e59df0
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Edge.java
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.apache.crunch.impl.mr.collect.PCollectionImpl;
+import org.apache.crunch.impl.mr.collect.PGroupedTableImpl;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ *
+ */
+class Edge {
+  private final Vertex head;
+  private final Vertex tail;
+  private final Set<NodePath> paths;
+  
+  public Edge(Vertex head, Vertex tail) {
+    this.head = head;
+    this.tail = tail;
+    this.paths = Sets.newHashSet();
+  }
+  
+  public Vertex getHead() {
+    return head;
+  }
+  
+  public Vertex getTail() {
+    return tail;
+  }
+
+  public void addNodePath(NodePath path) {
+    this.paths.add(path);
+  }
+  
+  public void addAllNodePaths(Collection<NodePath> paths) {
+    this.paths.addAll(paths);
+  }
+  
+  public Set<NodePath> getNodePaths() {
+    return paths;
+  }
+  
+  public PCollectionImpl getSplit() {
+    List<Iterator<PCollectionImpl<?>>> iters = Lists.newArrayList();
+    for (NodePath nodePath : paths) {
+      Iterator<PCollectionImpl<?>> iter = nodePath.iterator();
+      iter.next(); // prime this past the initial NGroupedTableImpl
+      iters.add(iter);
+    }
+
+    // Find the lowest point w/the lowest cost to be the split point for
+    // all of the dependent paths.
+    boolean end = false;
+    int splitIndex = -1;
+    while (!end) {
+      splitIndex++;
+      PCollectionImpl<?> current = null;
+      for (Iterator<PCollectionImpl<?>> iter : iters) {
+        if (iter.hasNext()) {
+          PCollectionImpl<?> next = iter.next();
+          if (next instanceof PGroupedTableImpl) {
+            end = true;
+            break;
+          } else if (current == null) {
+            current = next;
+          } else if (current != next) {
+            end = true;
+            break;
+          }
+        } else {
+          end = true;
+          break;
+        }
+      }
+    }
+    // TODO: Add costing calcs here.
+    
+    return Iterables.getFirst(paths, null).get(splitIndex);
+  }
+  
+  @Override
+  public boolean equals(Object other) {
+    if (other == null || !(other instanceof Edge)) {
+      return false;
+    }
+    Edge e = (Edge) other;
+    return head.equals(e.head) && tail.equals(e.tail) && paths.equals(e.paths);
+  }
+  
+  @Override
+  public int hashCode() {
+    return new HashCodeBuilder().append(head).append(tail).toHashCode();
+  }
+  
+  @Override
+  public String toString() {
+    return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Graph.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Graph.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Graph.java
new file mode 100644
index 0000000..ce0a847
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/Graph.java
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.crunch.Pair;
+import org.apache.crunch.impl.mr.collect.PCollectionImpl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+/**
+ *
+ */
+class Graph implements Iterable<Vertex> {
+
+  private final Map<PCollectionImpl, Vertex> vertices;
+  private final Map<Pair<Vertex, Vertex>, Edge> edges;  
+  private final Map<Vertex, List<Vertex>> dependencies;
+  
+  public Graph() {
+    this.vertices = Maps.newHashMap();
+    this.edges = Maps.newHashMap();
+    this.dependencies = Maps.newHashMap();
+  }
+  
+  public Vertex getVertexAt(PCollectionImpl impl) {
+    return vertices.get(impl);
+  }
+  
+  public Vertex addVertex(PCollectionImpl impl, boolean output) {
+    if (vertices.containsKey(impl)) {
+      Vertex v = vertices.get(impl);
+      if (output) {
+        v.setOutput();
+      }
+      return v;
+    }
+    Vertex v = new Vertex(impl);
+    vertices.put(impl, v);
+    if (output) {
+      v.setOutput();
+    }
+    return v;
+  }
+  
+  public Edge getEdge(Vertex head, Vertex tail) {
+    Pair<Vertex, Vertex> p = Pair.of(head, tail);
+    if (edges.containsKey(p)) {
+      return edges.get(p);
+    }
+    
+    Edge e = new Edge(head, tail);
+    edges.put(p, e);
+    tail.addIncoming(e);
+    head.addOutgoing(e);
+    return e;
+  }
+  
+  @Override
+  public Iterator<Vertex> iterator() {
+    return Sets.newHashSet(vertices.values()).iterator();
+  }
+
+  public Set<Edge> getAllEdges() {
+    return Sets.newHashSet(edges.values());
+  }
+  
+  public void markDependency(Vertex child, Vertex parent) {
+    List<Vertex> parents = dependencies.get(child);
+    if (parents == null) {
+      parents = Lists.newArrayList();
+      dependencies.put(child, parents);
+    }
+    parents.add(parent);
+  }
+  
+  public List<Vertex> getParents(Vertex child) {
+    if (dependencies.containsKey(child)) {
+      return dependencies.get(child);
+    }
+    return ImmutableList.of();
+  }
+  
+  public List<List<Vertex>> connectedComponents() {
+    List<List<Vertex>> components = Lists.newArrayList();
+    Set<Vertex> unassigned = Sets.newHashSet(vertices.values());
+    while (!unassigned.isEmpty()) {
+      Vertex base = unassigned.iterator().next();
+      List<Vertex> component = Lists.newArrayList();
+      component.add(base);
+      unassigned.remove(base);
+      Set<Vertex> working = Sets.newHashSet(base.getAllNeighbors());
+      while (!working.isEmpty()) {
+        Vertex n = working.iterator().next();
+        working.remove(n);
+        if (unassigned.contains(n)) {
+          component.add(n);
+          unassigned.remove(n);
+          for (Vertex n2 : n.getAllNeighbors()) {
+            if (unassigned.contains(n2)) {
+              working.add(n2);
+            }
+          }
+        }
+      }
+      components.add(component);
+    }
+    
+    return components;
+  }  
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/GraphBuilder.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/GraphBuilder.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/GraphBuilder.java
new file mode 100644
index 0000000..925c39a
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/GraphBuilder.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import org.apache.crunch.impl.mr.collect.DoCollectionImpl;
+import org.apache.crunch.impl.mr.collect.DoTableImpl;
+import org.apache.crunch.impl.mr.collect.InputCollection;
+import org.apache.crunch.impl.mr.collect.PCollectionImpl;
+import org.apache.crunch.impl.mr.collect.PGroupedTableImpl;
+import org.apache.crunch.impl.mr.collect.UnionCollection;
+
+/**
+ *
+ */
+class GraphBuilder implements PCollectionImpl.Visitor {
+
+  private Graph graph = new Graph();
+  private Vertex workingVertex;
+  private NodePath workingPath;
+  
+  public Graph getGraph() {
+    return graph;
+  }
+  
+  public void visitOutput(PCollectionImpl<?> output) {
+    workingVertex = graph.addVertex(output, true);
+    workingPath = new NodePath();
+    output.accept(this);
+  }
+  
+  @Override
+  public void visitInputCollection(InputCollection<?> collection) {
+    Vertex v = graph.addVertex(collection, false);
+    graph.getEdge(v, workingVertex).addNodePath(workingPath.close(collection));
+  }
+
+  @Override
+  public void visitUnionCollection(UnionCollection<?> collection) {
+    Vertex baseVertex = workingVertex;
+    NodePath basePath = workingPath;
+    for (PCollectionImpl<?> parent : collection.getParents()) {
+      workingPath = new NodePath(basePath);
+      workingVertex = baseVertex;
+      processParent(parent);
+    }
+  }
+
+  @Override
+  public void visitDoFnCollection(DoCollectionImpl<?> collection) {
+    workingPath.push(collection);
+    processParent(collection.getOnlyParent());
+  }
+
+  @Override
+  public void visitDoTable(DoTableImpl<?, ?> collection) {
+    workingPath.push(collection);
+    processParent(collection.getOnlyParent());
+  }
+
+  @Override
+  public void visitGroupedTable(PGroupedTableImpl<?, ?> collection) {
+    Vertex v = graph.addVertex(collection, false);
+    graph.getEdge(v, workingVertex).addNodePath(workingPath.close(collection));
+    workingVertex = v;
+    workingPath = new NodePath(collection);
+    processParent(collection.getOnlyParent());
+  }
+  
+  private void processParent(PCollectionImpl<?> parent) {
+    Vertex v = graph.getVertexAt(parent);
+    if (v == null) {
+      parent.accept(this);
+    } else {
+      graph.getEdge(v, workingVertex).addNodePath(workingPath.close(parent));
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/crunch/blob/890e0086/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/JobNameBuilder.java
----------------------------------------------------------------------
diff --git a/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/JobNameBuilder.java b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/JobNameBuilder.java
new file mode 100644
index 0000000..9ad7300
--- /dev/null
+++ b/crunch-core/src/main/java/org/apache/crunch/impl/mr/plan/JobNameBuilder.java
@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.crunch.impl.mr.plan;
+
+import java.util.List;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+
+/**
+ * Visitor that traverses the {@code DoNode} instances in a job and builds a
+ * String that identifies the stages of the pipeline that belong to this job.
+ */
+class JobNameBuilder {
+
+  private static final Joiner JOINER = Joiner.on("+");
+  private static final Joiner CHILD_JOINER = Joiner.on("/");
+
+  private String pipelineName;
+  List<String> rootStack = Lists.newArrayList();
+
+  public JobNameBuilder(final String pipelineName) {
+    this.pipelineName = pipelineName;
+  }
+
+  public void visit(DoNode node) {
+    visit(node, rootStack);
+  }
+
+  public void visit(List<DoNode> nodes) {
+    visit(nodes, rootStack);
+  }
+
+  private void visit(List<DoNode> nodes, List<String> stack) {
+    if (nodes.size() == 1) {
+      visit(nodes.get(0), stack);
+    } else {
+      List<String> childStack = Lists.newArrayList();
+      for (int i = 0; i < nodes.size(); i++) {
+        DoNode node = nodes.get(i);
+        List<String> subStack = Lists.newArrayList();
+        visit(node, subStack);
+        if (!subStack.isEmpty()) {
+          childStack.add("[" + JOINER.join(subStack) + "]");
+        }
+      }
+      if (!childStack.isEmpty()) {
+        stack.add("[" + CHILD_JOINER.join(childStack) + "]");
+      }
+    }
+  }
+
+  private void visit(DoNode node, List<String> stack) {
+    String name = node.getName();
+    if (!name.isEmpty()) {
+      stack.add(node.getName());
+    }
+    visit(node.getChildren(), stack);
+  }
+
+  public String build() {
+    return String.format("%s: %s", pipelineName, JOINER.join(rootStack));
+  }
+}