3
0

Enhance graph node's execution context

Each node receives their parent and child nodes for a simpler
exec context logic.
This commit is contained in:
Tomaž Jerman
2020-08-22 18:39:52 +02:00
committed by Tomaž Jerman
parent 47cc551d57
commit 5528449a64
2 changed files with 143 additions and 83 deletions
+102 -30
View File
@@ -17,8 +17,8 @@ type (
// When it comes to larger setups (custom CRM) the number of nodes and relations is well below 1000.
// Due to the simple interface, we can define a more optimal graph implementation as a sepperate module.
Graph struct {
// Nodes defines a set of all available structures conforming to the Node interface
Nodes []Node
// nodes defines a set of all available structures conforming to the Node interface
nodes []Node
// Since everything is calculated on-the-fly, we need a simple boolean flag to determine if the graph is inverted
invert bool
// Allows us to keep track of all the nodes that were determined as conflicting.
@@ -26,11 +26,10 @@ type (
// If the data input is consistent, the conflicting nodes will be consistent, but there is no guarantee
// what node in a cycle will be selected as a conflicting.
conflicts set
// Allows us to easily keep track of nodes that were already processed.
processed set
}
// NodeRelation specifies what nodes from what resource this node is in relation with.
// The mapping is in the form of { resource: [nodeID, nodeID, ...] }
NodeRelation map[string][]string
// A simple "set" implementation for simpler, quicker checks
set map[string]bool
@@ -48,42 +47,74 @@ type (
// Relations provides a list of node's relations
// This can be calculated on the fly based on the node's state and don't need to be
// built in to the node struct.
Relations() NodeRelation
Relations() map[string][]string
// Run should implement the actual operation that should be performend when the node is invoked.
// This can be as simple or as complex as needed
Run(context.Context) error
// It's also provided a set of child and parent nodes so we can easily provide node context
Run(ctx context.Context, cc []Node, pp []Node) error
// ResolveConflict should implement any operation that should occur when the node
// causes a dependency conflict. For example -- partially import the records (without relations)
// and correct those relations when executing the Run function
ResolveConflict(context.Context) error
// It's also provided a set of child and parent nodes so we can easily provide node context
ResolveConflict(ctx context.Context, cc []Node, pp []Node) error
}
)
// Add registers a given set of nodes nn into the graph g
func (g *Graph) Add(nn ...Node) {
g.Nodes = append(g.Nodes, nn...)
g.nodes = append(g.nodes, nn...)
}
// Remove removes the set of nodes nn from the graph h
func (g *Graph) Remove(nn ...Node) {
mm := make([]Node, 0, len(g.Nodes)-len(nn))
for _, m := range g.Nodes {
mh := NodeHash(m)
if len(nn) <= 0 {
return
}
mm := make([]Node, 0, len(g.nodes))
for _, m := range g.nodes {
for _, n := range nn {
if mh != NodeHash(n) {
mm = append(mm, m)
if NodeHash(m) == NodeHash(n) && g.canRemove(n) {
goto skip
}
}
mm = append(mm, m)
skip:
}
g.nodes = mm
}
func (g *Graph) canRemove(n Node) bool {
if len(g.nodes) <= 1 {
return true
}
// When the first unprocessed child node is found, terminate
for _, m := range g.Children(n) {
if !g.processed[NodeHash(m)] {
return false
}
}
return true
}
func (g *Graph) MarkProcessed(nn ...Node) {
if g.processed == nil {
g.processed = make(set)
}
for _, n := range nn {
g.processed[NodeHash(n)] = true
}
g.Nodes = mm
}
// FindNode returns all nodes that match the given resource and identifiers
func (g *Graph) FindNode(res string, IDs ...string) []Node {
nn := make([]Node, 0, len(IDs))
for _, n := range g.Nodes {
for _, n := range g.nodes {
for _, ID := range IDs {
if NodeHash(n) == NodeHashRaw(res, ID) {
nn = append(nn, n)
@@ -108,15 +139,24 @@ func (g *Graph) SetNodeConflict(n Node) {
g.conflicts[NodeHash(n)] = true
}
func (g *Graph) removeProcessed(nn []Node) []Node {
mm := make([]Node, 0, len(nn))
for _, n := range nn {
if !g.processed[NodeHash(n)] {
mm = append(mm, n)
}
}
return mm
}
// Children provides a list of children of the node n
//
// Child nodes are calculated on the fly based on node Relations()
func (g *Graph) Children(n Node) []Node {
if !g.invert {
return g.children(n)
return g.removeProcessed(g.children(n))
}
return g.parents(n)
return g.removeProcessed(g.parents(n))
}
func (g *Graph) children(n Node) []Node {
@@ -128,22 +168,32 @@ func (g *Graph) children(n Node) []Node {
return nn
}
func (g *Graph) ChildrenA(n Node) []Node {
if !g.invert {
return g.children(n)
}
return g.parents(n)
}
// Parents provides a list of parents of the node n
//
// Parent nodes are calculated on the fly based on node Relations()
func (g *Graph) Parents(n Node) []Node {
if !g.invert {
return g.parents(n)
return g.removeProcessed(g.parents(n))
}
return g.children(n)
return g.removeProcessed(g.children(n))
}
func (g *Graph) parents(n Node) []Node {
nn := make([]Node, 0)
for _, m := range g.Nodes {
if IDs, has := m.Relations()[n.Resource()]; has {
for _, m := range g.nodes {
r := m.Relations()
if r == nil {
continue
}
if IDs, has := r[n.Resource()]; has {
for _, ID := range IDs {
if n.ID() == ID {
nn = append(nn, m)
@@ -169,12 +219,29 @@ func (g *Graph) ParentsC(n Node) []Node {
return mm
}
func (g *Graph) ParentsA(n Node) []Node {
if !g.invert {
return g.parents(n)
}
return g.children(n)
}
// Validate performs a basic data validation over all the nodes.
func (g *Graph) Validate() error {
// @todo...
return nil
}
func (g *Graph) Nodes() []Node {
nn := make([]Node, 0, len(g.nodes))
for _, n := range g.nodes {
if !g.processed[NodeHash(n)] {
nn = append(nn, n)
}
}
return nn
}
// Run invokes all operations while respecting relations (dependencies) and solving
// dependency conflicts.
//
@@ -185,10 +252,10 @@ func (g *Graph) Validate() error {
//
// The order of above operations is a bit more complex, but the general flow is that.
func (g *Graph) Run(ctx context.Context) error {
for len(g.Nodes) > 0 {
for len(g.nodes) > 0 {
// Find all root nodes in the current graph state; those nodes are allowed to run.
nn := make([]Node, 0, len(g.Nodes)/2)
for _, n := range g.Nodes {
nn := make([]Node, 0, len(g.Nodes())/2)
for _, n := range g.Nodes() {
// We should not take into account conflicted parent nodes, as they already resolved
// the conflict.
if len(g.ParentsC(n)) == 0 {
@@ -216,11 +283,16 @@ func (g *Graph) Run(ctx context.Context) error {
// are allowed to run.
func (g *Graph) runRegular(ctx context.Context, nn []Node) error {
for _, n := range nn {
err := n.Run(ctx)
err := n.Run(ctx, g.Children(n), g.ParentsA(n))
if err != nil {
return err
}
// When a child node is processed check if it's parent can be removed.
// A parent node can only be removed if all of it's child nodes have already been
// processed
g.MarkProcessed(n)
g.Remove(g.ParentsA(n)...)
g.Remove(n)
}
@@ -240,14 +312,14 @@ func (g *Graph) runResolution(ctx context.Context) error {
// For example: A -> B -> A -> c -> D; where A B C is the cycle and C is a branch from the cycle.
// The code will works just fine, but it won't be that optimal so it should be improved to do
// actual cycle detection.
for _, m := range g.Nodes {
for _, m := range g.Nodes() {
if !g.conflicts[NodeHash(m)] {
n = m
break
}
}
err := n.ResolveConflict(ctx)
err := n.ResolveConflict(ctx, g.Children(n), g.Parents(n))
if err != nil {
return err
}
+41 -53
View File
@@ -12,7 +12,7 @@ var (
type (
NodeTest struct {
Id string
relations NodeRelation
relations map[string][]string
exOrder *[]string
}
)
@@ -25,16 +25,16 @@ func (n *NodeTest) Resource() string {
return "test"
}
func (n *NodeTest) Relations() NodeRelation {
func (n *NodeTest) Relations() map[string][]string {
return n.relations
}
func (n *NodeTest) Run(ctx context.Context) error {
func (n *NodeTest) Run(ctx context.Context, cc []Node, pp []Node) error {
ops = append(ops, "R:"+n.ID())
return nil
}
func (n *NodeTest) ResolveConflict(ctx context.Context) error {
func (n *NodeTest) ResolveConflict(ctx context.Context, cc []Node, pp []Node) error {
ops = append(ops, "C:"+n.ID())
return nil
}
@@ -50,8 +50,8 @@ func TestGraphStructure_relations(t *testing.T) {
{
name: "N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2"}}},
{Id: "N2", relations: map[string][]string{}},
},
node: "N1",
children: []string{"N2"},
@@ -60,8 +60,8 @@ func TestGraphStructure_relations(t *testing.T) {
{
name: "N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2"}}},
{Id: "N2", relations: map[string][]string{}},
},
node: "N2",
children: []string{},
@@ -70,7 +70,7 @@ func TestGraphStructure_relations(t *testing.T) {
{
name: "N1 -> N1 :: cycle to self",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N1"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N1"}}},
},
node: "N1",
children: []string{"N1"},
@@ -79,8 +79,8 @@ func TestGraphStructure_relations(t *testing.T) {
{
name: "N1 -> N1 -> N2 <- N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N1", "N2"}}},
{Id: "N2", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N1", "N2"}}},
{Id: "N2", relations: map[string][]string{"test": []string{"N2"}}},
},
node: "N1",
children: []string{"N1", "N2"},
@@ -89,8 +89,8 @@ func TestGraphStructure_relations(t *testing.T) {
{
name: "N1 -> N1 -> N2 <- N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N1", "N2"}}},
{Id: "N2", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N1", "N2"}}},
{Id: "N2", relations: map[string][]string{"test": []string{"N2"}}},
},
node: "N2",
children: []string{"N2"},
@@ -104,8 +104,8 @@ func TestGraphStructure_relations(t *testing.T) {
g.Add(n)
}
cc := g.Children(g.findNode("test", test.node)[0])
pp := g.Parents(g.findNode("test", test.node)[0])
cc := g.Children(g.FindNode("test", test.node)[0])
pp := g.Parents(g.FindNode("test", test.node)[0])
if len(cc) != len(test.children) {
t.Errorf("[%s] node child missmatch; list range doesnt match; exp=%d got=%d", test.name, len(test.children), len(cc))
@@ -142,8 +142,8 @@ func TestGraphStructure_inversion(t *testing.T) {
{
name: "N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2"}}},
{Id: "N2", relations: map[string][]string{}},
},
node: "N1",
children: []string{},
@@ -152,8 +152,8 @@ func TestGraphStructure_inversion(t *testing.T) {
{
name: "N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2"}}},
{Id: "N2", relations: map[string][]string{}},
},
node: "N2",
children: []string{"N1"},
@@ -162,7 +162,7 @@ func TestGraphStructure_inversion(t *testing.T) {
{
name: "N1 -> N1 :: cycle to self",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N1"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N1"}}},
},
node: "N1",
children: []string{"N1"},
@@ -178,8 +178,8 @@ func TestGraphStructure_inversion(t *testing.T) {
g.Invert()
cc := g.Children(g.findNode("test", test.node)[0])
pp := g.Parents(g.findNode("test", test.node)[0])
cc := g.Children(g.FindNode("test", test.node)[0])
pp := g.Parents(g.FindNode("test", test.node)[0])
if len(cc) != len(test.children) {
t.Errorf("[%s] node child missmatch; list range doesnt match; exp=%d got=%d", test.name, len(test.children), len(cc))
@@ -214,33 +214,40 @@ func TestGraphStructure_execution(t *testing.T) {
{
name: "N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2"}}},
{Id: "N2", relations: map[string][]string{}},
},
ops: []string{"R:N2", "R:N1"},
},
{
name: "N1 -> N1",
nodes: []*NodeTest{
{Id: "N1", relations: map[string][]string{"test": []string{"N1"}}},
},
ops: []string{"C:N1", "R:N1"},
},
{
name: "N1 -> N1 -> N2",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2", "N1"}}},
{Id: "N2", relations: NodeRelation{}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2", "N1"}}},
{Id: "N2", relations: map[string][]string{}},
},
ops: []string{"R:N2", "C:N1", "R:N1"},
},
{
name: "N2 -> N1 -> N1",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N1"}}},
{Id: "N2", relations: NodeRelation{"test": []string{"N1"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N1"}}},
{Id: "N2", relations: map[string][]string{"test": []string{"N1"}}},
},
ops: []string{"C:N1", "R:N1", "R:N2"},
},
{
name: "N1 -> N1 <-> N2 <-> N3 <- N1",
nodes: []*NodeTest{
{Id: "N1", relations: NodeRelation{"test": []string{"N2", "N1", "N3"}}},
{Id: "N2", relations: NodeRelation{"test": []string{"N1", "N3"}}},
{Id: "N3", relations: NodeRelation{"test": []string{"N2"}}},
{Id: "N1", relations: map[string][]string{"test": []string{"N2", "N1", "N3"}}},
{Id: "N2", relations: map[string][]string{"test": []string{"N1", "N3"}}},
{Id: "N3", relations: map[string][]string{"test": []string{"N2"}}},
},
ops: []string{"C:N1", "C:N2", "R:N3", "R:N1", "R:N2"},
},
@@ -253,6 +260,9 @@ func TestGraphStructure_execution(t *testing.T) {
g.Add(n)
}
// spew.Dump(g.nodes)
// spew.Dump("___", ops)
g.Invert()
g.Run(context.Background())
@@ -261,27 +271,5 @@ func TestGraphStructure_execution(t *testing.T) {
t.Errorf("[%s] operation missmatch; exp=%s got=%s", test.name, o, ops[i])
}
}
// if len(cc) != len(test.children) {
// t.Errorf("[%s] node child missmatch; list range doesnt match; exp=%d got=%d", test.name, len(test.children), len(cc))
// return
// }
// for i, c := range cc {
// if c.ID() != test.children[i] {
// t.Errorf("[%s] node child missmatch; exp=%s got=%s pos=%d", test.name, test.children[i], c.ID(), i)
// return
// }
// }
// if len(pp) != len(test.parents) {
// t.Errorf("[%s] node parent missmatch; list range doesnt match; exp=%d got=%d", test.name, len(test.parents), len(pp))
// return
// }
// for i, p := range pp {
// if p.ID() != test.parents[i] {
// t.Errorf("[%s] node parent missmatch; exp=%s got=%s pos=%d", test.name, test.parents[i], p.ID(), i)
// return
// }
// }
}
}