Dependency graph resolution algorithm in Go

I’ve been continuing to improve Gru
over the past days and one thing I thought would be nice to have is a
way to express commands in Gru in a declarative way. That way I would
not have to bother about the underlying details about a minion and
have an easy way to express things, e.g. install packages, manage
services, deploy configurations, etc.

TL;DR

In this post we see how to resolve dependency graphs in Go by
implementing a simple, but efficient algorithm.

Looking at the above example configuration we can clearly see the
relation between our resources.

However one obvious issue arises once we start introducing dependencies
between our resources - and that is - how do we properly resolve them?

Most of the time resolving dependencies such as the one shown above
means dealing with the
Graph theory and
implementing an algorithm in order to resolve the dependency graph.

There are some good graph packages for Go such as the
gonum/graph package, but for my
specific case I wanted something as simple as possible without
having to pull lots of library dependencies, which would happen if I
have to use an external solution.

I liked the implementation - it is simple, easy to understand and
efficient. Thought I should translate it to Go.

What the algorithm below does is instead of traversing the graph
recursively, is instead iteratively finding nodes with no dependencies.

Every node with no dependencies is then removed from the graph. If at
any point in time there are still nodes in the graph, but we cannot
find nodes with no dependencies - then we have a
circular dependency in our graph.

// Node represents a single node in the graph with it's dependenciestypeNodestruct{// Name of the nodenamestring// Dependencies of the nodedeps[]string}// NewNode creates a new nodefuncNewNode(namestring,deps...string)*Node{n:=&Node{name:name,deps:deps,}returnn}

A graph represents a collection of nodes, so lets create a
type for the graph as well.

typeGraph[]*Node

We will also implement a function which will display the
dependency graph for us.

Now lets implement the iterative algorithm which will resolve the
graph.

// Resolves the dependency graphfuncresolveGraph(graphGraph)(Graph,error){// A map containing the node names and the actual node objectnodeNames:=make(map[string]*Node)// A map containing the nodes and their dependenciesnodeDependencies:=make(map[string]mapset.Set)// Populate the mapsfor_,node:=rangegraph{nodeNames[node.name]=nodedependencySet:=mapset.NewSet()for_,dep:=rangenode.deps{dependencySet.Add(dep)}nodeDependencies[node.name]=dependencySet}// Iteratively find and remove nodes from the graph which have no dependencies.// If at some point there are still nodes in the graph and we cannot find// nodes without dependencies, that means we have a circular dependencyvarresolvedGraphforlen(nodeDependencies)!=0{// Get all nodes from the graph which have no dependenciesreadySet:=mapset.NewSet()forname,deps:=rangenodeDependencies{ifdeps.Cardinality()==0{readySet.Add(name)}}// If there aren't any ready nodes, then we have a cicular dependencyifreadySet.Cardinality()==0{vargGraphforname:=rangenodeDependencies{g=append(g,nodeNames[name])}returng,errors.New("Circular dependency found")}// Remove the ready nodes and add them to the resolved graphforname:=rangereadySet.Iter(){delete(nodeDependencies,name.(string))resolved=append(resolved,nodeNames[name.(string)])}// Also make sure to remove the ready nodes from the// remaining node dependencies as wellforname,deps:=rangenodeDependencies{diff:=deps.Difference(readySet)nodeDependencies[name]=diff}}returnresolved,nil}

The above code uses a mapset.Set type which can be found in the
golang-set package.