code: public Dag<T> concatenate(Dag<T> other, Set<DagNode<T>> forkNodes) {
      if (other == null || other.isEmpty()) {
        return this;
      }
      if (this.isEmpty()) {
        return other;
      }
  
      for (DagNode node : getDependencyNodes(forkNodes)) {
        if (!this.parentChildMap.containsKey(node)) {
          this.parentChildMap.put(node, Lists.newArrayList());
        }
        for (DagNode otherNode : other.startNodes) {
          this.parentChildMap.get(node).add(otherNode);
          otherNode.addParentNode(node);
        }
      }
      //Each node which is a forkable node is added to list of end nodes of the concatenated dag
      other.endNodes.addAll(forkNodes);
      this.endNodes = other.endNodes;
  
      //Append all the entries from the other dag's parentChildMap to this dag's parentChildMap
      this.parentChildMap.putAll(other.parentChildMap);
  
      //If there exists a node in the other dag with no parent nodes, add it to the list of start nodes of the
      // concatenated dag.
      other.startNodes.stream().filter(node -> other.getParents(node).isEmpty())
          .forEach(node -> this.startNodes.add(node));
  
      this.nodes.addAll(other.nodes);
      return this;
    }
docs: Concatenate two dags together. Join the "other" dag to "this" dag and return "this" dag.
  The concatenate method ensures that all the jobs of "this" dag (which may have multiple end nodes)
  are completed before starting any job of the "other" dag. This is done by adding each endNode of this dag, which is
  not a fork node, as a parent of every startNode of the other dag.
  
  @param other dag to concatenate to this dag
  @param forkNodes a set of nodes from this dag which are marked as forkable nodes. Each of these nodes will be added
  to the list of end nodes of the concatenated dag. Essentially, a forkable node has no dependents
  in the concatenated dag.
  @return the concatenated dag