From 26e46a6e1893f2d7da806a640583bb1a1dddbe9e Mon Sep 17 00:00:00 2001 From: James Shubin Date: Mon, 22 Jan 2024 15:05:47 -0500 Subject: [PATCH] lang: funcs: txn: Add a simple graph implementation This fulfills the GraphAPI that we use. --- lang/funcs/txn/graph.go | 180 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 lang/funcs/txn/graph.go diff --git a/lang/funcs/txn/graph.go b/lang/funcs/txn/graph.go new file mode 100644 index 00000000..768eed46 --- /dev/null +++ b/lang/funcs/txn/graph.go @@ -0,0 +1,180 @@ +// Mgmt +// Copyright (C) 2013-2023+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package txn + +import ( + "sync" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/pgraph" +) + +var _ interfaces.GraphAPI = &Graph{} // ensure it meets this expectation + +// Graph is a simple pgraph wrapper that implements the GraphAPI interface. That +// interface is also implemented by *dage.Engine and the code is very similar. +type Graph struct { + Debug bool + Logf func(format string, v ...interface{}) + + graph *pgraph.Graph // guarded by graphMutex + graphMutex *sync.Mutex // TODO: &sync.RWMutex{} ? +} + +// Init initializes the struct before first use and returns it for ergonomics. +func (obj *Graph) Init() *Graph { + var err error + obj.graph, err = pgraph.NewGraph("graph") + if err != nil { + panic("graph was not built") + } + obj.graphMutex = &sync.Mutex{} // TODO: &sync.RWMutex{} ? + + return obj +} + +// AddVertex adds a vertex to the graph. It takes a lock. +func (obj *Graph) AddVertex(f interfaces.Func) error { + obj.graphMutex.Lock() + defer obj.graphMutex.Unlock() + if obj.Debug { + obj.Logf("AddVertex: %p %s", f, f) + } + + obj.graph.AddVertex(f) + return nil +} + +// AddEdge adds an edge to the graph. It takes a lock. +func (obj *Graph) AddEdge(f1, f2 interfaces.Func, fe *interfaces.FuncEdge) error { + obj.graphMutex.Lock() + defer obj.graphMutex.Unlock() + if obj.Debug { + obj.Logf("AddEdge %p %s: %p %s -> %p %s", fe, fe, f1, f1, f2, f2) + } + + // safety check to avoid cycles + g := obj.graph.Copy() + g.AddEdge(f1, f2, fe) + if _, err := g.TopologicalSort(); err != nil { + return err // not a dag + } + // if we didn't cycle, we can modify the real graph safely... + + obj.graph.AddEdge(f1, f2, fe) // replaces any existing edge here + + // This shouldn't error, since the test graph didn't find a cycle. + if _, err := obj.graph.TopologicalSort(); err != nil { + // programming error + panic(err) // not a dag + } + + return nil +} + +// DeleteVertex deletes a vertex from the graph. It takes a lock. +func (obj *Graph) DeleteVertex(f interfaces.Func) error { + obj.graphMutex.Lock() + defer obj.graphMutex.Unlock() + if obj.Debug { + obj.Logf("DeleteVertex: %p %s", f, f) + } + + obj.graph.DeleteVertex(f) + return nil +} + +// DeleteEdge deletes an edge from the graph. It takes a lock. +func (obj *Graph) DeleteEdge(fe *interfaces.FuncEdge) error { + obj.graphMutex.Lock() + defer obj.graphMutex.Unlock() + if obj.Debug { + f1, f2, found := obj.graph.LookupEdge(fe) + if found { + obj.Logf("DeleteEdge: %p %s -> %p %s", f1, f1, f2, f2) + } else { + obj.Logf("DeleteEdge: not found %p %s", fe, fe) + } + } + + // Don't bother checking if edge exists first and don't error if it + // doesn't because it might have gotten deleted when a vertex did, and + // so there's no need to complain for nothing. + obj.graph.DeleteEdge(fe) + return nil +} + +// HasVertex checks if a vertex exists in the graph. It takes a lock. +func (obj *Graph) HasVertex(f interfaces.Func) bool { + obj.graphMutex.Lock() // XXX: should this be a RLock? + defer obj.graphMutex.Unlock() // XXX: should this be an RUnlock? + + return obj.graph.HasVertex(f) +} + +// LookupEdge checks which vertices (if any) exist between an edge in the graph. +// It takes a lock. +func (obj *Graph) LookupEdge(fe *interfaces.FuncEdge) (interfaces.Func, interfaces.Func, bool) { + obj.graphMutex.Lock() // XXX: should this be a RLock? + defer obj.graphMutex.Unlock() // XXX: should this be an RUnlock? + + v1, v2, found := obj.graph.LookupEdge(fe) + if !found { + return nil, nil, found + } + f1, ok := v1.(interfaces.Func) + if !ok { + panic("not a Func") + } + f2, ok := v2.(interfaces.Func) + if !ok { + panic("not a Func") + } + return f1, f2, found +} + +// FindEdge checks which edge (if any) exists between two vertices in the graph. +// It takes a lock. This is an important method in edge removal, because it's +// what you really need to know for DeleteEdge to work. Requesting a specific +// deletion isn't very sensical in this library when specified as the edge +// pointer, since we might replace it with a new edge that has new arg names. +// Instead, use this to look up what relationship you want, and then DeleteEdge +// to remove it. +func (obj *Graph) FindEdge(f1, f2 interfaces.Func) *interfaces.FuncEdge { + obj.graphMutex.Lock() // XXX: should this be a RLock? + defer obj.graphMutex.Unlock() // XXX: should this be an RUnlock? + + edge := obj.graph.FindEdge(f1, f2) + if edge == nil { + return nil + } + fe, ok := edge.(*interfaces.FuncEdge) + if !ok { + panic("edge is not a FuncEdge") + } + + return fe +} + +// Graph returns a copy of the contained graph. It takes a lock. +func (obj *Graph) Graph() *pgraph.Graph { + obj.graphMutex.Lock() // XXX: should this be a RLock? + defer obj.graphMutex.Unlock() // XXX: should this be an RUnlock? + + return obj.graph.Copy() +}