// Package graph provides a graph or interconnected state-machines and their // states, based on the dbg telemetry protocol.
package graph // TODO fix GC import ( amhelp am ssrpc ssdbg ) var ErrGraph = errors.New("graph error") // AddErrGraph adds [ErrGraph]. func ( *am.Event, *am.Machine, error, ...am.A, ) am.Result { if == nil { return am.Executed } = fmt.Errorf("%w: %w", ErrGraph, ) return .EvAddErrState(, ss.ErrGraph, , am.OptArgs()) } var ss = ssdbg.ServerStates type Vertex struct { StateName string MachId string } type Edge = graph.Edge[*Vertex] type EdgeData struct { // machine has a state MachHas *MachineHas // machine has an RPC connection to another machine MachConnectedTo bool // machine is a child of another machine MachChildOf bool // machine has pipes going to another machine MachPipesTo []*MachPipeTo // state has relations with other states StateRelation []*StateRelation } // Client: // - has State inherited:string auto:bool multi:bool // - connectedTo Client addr:string // - pipeTo Client states:map[string]string // - childOf Client // // State: // - relation type:require|add|remove State // - pipeTo Client|state add:bool type MachineHas struct { Inherited string Auto bool Multi bool } type StateRelation struct { RelType am.Relation } // TODO use am.Pipe type MachPipeTo struct { FromState string ToState string MutType am.MutationType } type Connection struct { Edge *EdgeData Source *Vertex Target *Vertex } func hash( *Vertex) string { if .StateName != "" { return .MachId + ":" + .StateName } return .MachId } // Client represents a single state machine withing the network graph. type Client struct { Id string // TODO version schemas MsgSchema *dbg.DbgMsgStruct LatestMsgTx *dbg.DbgMsgTx LatestTimeSum uint64 LatestMTime am.Time ConnId string } // ///// ///// ///// // ///// GRAPH // ///// ///// ///// type Graph struct { Server *am.Machine Clients map[string]*Client // G is a directed graph of machines and states with metadata. G graph.Graph[string, *Vertex] // Map is a unidirectional mirror of g, without metadata. Map graph.Graph[string, *Vertex] } func ( *am.Machine) (*Graph, error) { if !.Has(ssdbg.ServerStates.Names()) { return nil, fmt.Errorf( "Graph.New: server machine %s does not implement ssdbg.ServerStates", .Id(), ) } := &Graph{ Server: , G: graph.New(hash, graph.Directed()), Map: graph.New(hash), Clients: make(map[string]*Client), } // err := m.BindHandlers(g) // if err != nil { // return nil, err // } return , nil } // Clone returns a deep clone of the graph. func ( *Graph) () (*Graph, error) { , := .G.Clone() if != nil { return nil, } , := .Map.Clone() if != nil { return nil, } := &Graph{ G: , Map: , Clients: make(map[string]*Client, len(.Clients)), } for , := range .Clients { .Clients[] = &Client{ Id: , MsgSchema: .MsgSchema, LatestMTime: .LatestMTime, LatestTimeSum: .LatestTimeSum, } } return , nil } func ( *Graph) () { .Clients = make(map[string]*Client) .G = graph.New(hash, graph.Directed()) .Map = graph.New(hash) } // Connection returns a Connection for the given source-target. func ( *Graph) (, string) (*Connection, error) { , := .G.Edge(, ) if != nil { return nil, } := .Properties.Data.(*EdgeData) , := .G.Vertex() if != nil { return nil, } , := .G.Vertex() if != nil { return nil, } return &Connection{ Edge: , Source: , Target: , }, nil } func ( *Graph) ( string, *dbg.DbgMsgTx) { := .Clients[] var uint64 for , := range .Clocks { += } := .MsgSchema.StatesIndex // optimize space if len(.CalledStates) > 0 { .CalledStatesIdxs = amhelp.StatesToIndexes(, .CalledStates) .MachineID = "" .CalledStates = nil } // detect RPC connections - read arg "id" for HandshakeDone, being the ID of // the RPC client // TODO extract to a func if .LatestMsgTx != nil { := .LatestMsgTx := &am.Transition{ TimeBefore: .Clocks, TimeAfter: .Clocks, } , , := amhelp.GetTransitionStates(, ) // RPC conns (requires LogLevel2) := slices.Contains(.MsgSchema.Tags, "rpc-server") if slices.Contains(, ssrpc.ServerStates.HandshakeDone) && { for , := range .LogEntries { if !strings.HasPrefix(.Text, "[add] ") { continue } := strings.Split(strings.TrimRight(.Text, ")\n"), "(") for , := range strings.Split([1], " ") { := strings.Split(, "=") if [0] != "id" { continue } := [1] := graph.EdgeData(&EdgeData{MachConnectedTo: true}) := .G.AddEdge(, .Id, ) if != nil { // wait for the other mach to show up TODO use addMach() // TODO leaks, use addMach() .Server.Log("waiting for RPC conn %s to show up", ) := .Server.WhenArgs(ss.InitClient, am.A{"id": [1]}, nil) go func() { <- .Server.Log("Resuming RPC for %s", ) if := .G.AddEdge([1], .Id, ); != nil { AddErrGraph(nil, .Server, fmt.Errorf("ParseMsg: %w", )) return } if = .Map.AddEdge([1], .Id); != nil { AddErrGraph(nil, .Server, fmt.Errorf("ParseMsg: %w", )) return } }() } else { if = .Map.AddEdge([1], .Id); != nil { AddErrGraph(nil, .Server, fmt.Errorf("ParseMsg: %w", )) return } } } } } } // TODO errors // var isErr bool // for _, name := range index { // if strings.HasPrefix(name, "Err") && msgTx.Is1(index, name) { // isErr = true // break // } // } // if isErr || msgTx.Is1(index, am.StateException) { // // prepend to errors TODO DB errors // // idx := SQL COUNT // c.errors = append([]int{idx}, c.errors...) // } := .parseMsgLog(, ) if != nil { AddErrGraph(nil, .Server, fmt.Errorf("parseMsgLog: %w", )) } // TODO dedicated error state, enable once stable // if err != nil { // g.Mach.AddErr(fmt.Errorf("Graph.parseMsgLog: %w", err), nil) // } .LatestMsgTx = // TODO assert clocks .LatestMTime = .Clocks .LatestTimeSum = } func ( *Graph) ( string) error { // TODO return nil } func ( *Graph) ( *dbg.DbgMsgStruct) error { // init := .ID := &Client{ Id: , MsgSchema: , LatestMTime: make(am.Time, len(.States)), } .Clients[] = // add machine TODO dont exit on error, merge var error if = .addMach(.Id); != nil { return } // parent if .MsgSchema.Parent != "" { := graph.EdgeData(&EdgeData{MachChildOf: true}) = .G.AddEdge(.Id, .MsgSchema.Parent, ) if != nil { // wait for the parent to show up TODO use addMach() .Server.Log("waiting for parent %s to show up", .MsgSchema.Parent) := .Server.WhenArgs(ss.InitClient, am.A{"id": .MsgSchema.Parent}, nil) go func() { <- .Server.Log("resuming for %s", .MsgSchema.Parent) = .G.AddEdge(.Id, .MsgSchema.Parent, ) if == nil { _ = .Map.AddEdge(.Id, .MsgSchema.Parent) } AddErrGraph(nil, .Server, fmt.Errorf("AddClient: %w", )) }() } else { _ = .Map.AddEdge(.Id, .MsgSchema.Parent) } } // add states for , := range .MsgSchema.States { // vertex = .G.AddVertex(&Vertex{ MachId: , StateName: , }) if != nil { return } _ = .Map.AddVertex(&Vertex{ MachId: , StateName: , }) // edge = .G.AddEdge(, +":"+, graph.EdgeData(&EdgeData{ MachHas: &MachineHas{ Auto: .Auto, Multi: .Multi, // TODO Inherited: "", }, })) if != nil { return } _ = .Map.AddEdge(, +":"+) } // DEBUG // if c.Id == "rc-srv-browser2" { // ver, _ := g.G.Vertex("rc-srv-browser2") // ver = ver // print() // } type struct { am.S am.Relation } // add relations for , := range .MsgSchema.States { // define := []{ {: .Require, : am.RelationRequire}, {: .Add, : am.RelationAdd}, {: .Remove, : am.RelationRemove}, } // per relation for , := range { // per state for , := range . { := + ":" + := + ":" + // update an existing edge if , := .G.Edge(, ); == nil { := .Properties.Data.(*EdgeData) .StateRelation = append(.StateRelation, &StateRelation{ RelType: ., }) = .G.UpdateEdge(, , graph.EdgeData()) if != nil { return } continue } // add if doesnt exist = .G.AddEdge(, , graph.EdgeData(&EdgeData{ StateRelation: []*StateRelation{ {RelType: .}, }, })) if != nil { return } _ = .Map.AddEdge(, ) } } } return nil } func ( *Graph) ( string) error { := .G.AddVertex(&Vertex{ MachId: , }) if errors.Is(, graph.ErrVertexAlreadyExists) { return nil } if != nil { return } _ = .Map.AddVertex(&Vertex{ MachId: , }) return } // DumpGv will create a dot-format *.gv file of the graph. To create an SVG: // // dot -Tsvg -O path func ( *Graph) ( string) error { , := os.Create() if != nil { return } return draw.DOT(.G, ) } type MachInspect struct { Child string States []string Conns []string Pipes map[string][]string Time uint64 Schema am.Schema Tags []string MTime am.Time } func ( *Graph) () (map[string]*MachInspect, error) { , := .G.AdjacencyMap() if != nil { return nil, } := make(map[string]*MachInspect) for , := range { if len(strings.Split(, ":")) == 2 { continue } , := .Clients[] if ! { // TODO err continue } // init _, = [] if ! { [] = &MachInspect{ Pipes: make(map[string][]string), Time: .LatestTimeSum, Schema: .MsgSchema.States.Clone(), Tags: slices.Clone(.MsgSchema.Tags), MTime: slices.Clone(.LatestMTime), States: slices.Clone(.MsgSchema.StatesIndex), } } for , := range { , := .Connection(, .Target) if .Edge.MachChildOf { [].Child = .Target } if .Edge.MachConnectedTo { [].Conns = append([].Conns, .Target) } if .Edge.MachPipesTo != nil { for , := range .Edge.MachPipesTo { [].Pipes[.Target] = append( [].Pipes[.Target], fmt.Sprintf( "[%s] %s -> %s", .MutType, .FromState, .ToState, ), ) } } } } return , nil } // private func ( *Graph) ( *Client, *dbg.DbgMsgTx) error { // pre-tx log entries for , := range .PreLogEntries { := .parseMsgReader(, , ) if != nil { return } } // tx log entries for , := range .LogEntries { := .parseMsgReader(, , ) if != nil { return } } return nil } func ( *Graph) ( *Client, *am.LogEntry, *dbg.DbgMsgTx, ) error { // NEW PIPE if strings.HasPrefix(.Text, "[pipe-in:add] ") || strings.HasPrefix(.Text, "[pipe-in:remove] ") || strings.HasPrefix(.Text, "[pipe-out:add] ") || strings.HasPrefix(.Text, "[pipe-out:remove] ") { := strings.HasPrefix(.Text, "[pipe-in:add] ") || strings.HasPrefix(.Text, "[pipe-out:add] ") := strings.HasPrefix(.Text, "[pipe-out") var []string if && { = strings.Split(.Text[len("[pipe-out:add] "):], " to ") } else if ! && { = strings.Split(.Text[len("[pipe-in:add] "):], " from ") } else if && ! { = strings.Split(.Text[len("[pipe-out:remove] "):], " to ") } else if ! && ! { = strings.Split(.Text[len("[pipe-in:remove] "):], " from ") } := am.MutationRemove if { = am.MutationAdd } // define what we know from this log line := [0] := [1] var string var string if { = .Id = } else { = = .Id } // get edge , := .G.Edge(, ) var *EdgeData if != nil { = &EdgeData{} := .G.AddEdge(, , graph.EdgeData()) if != nil { // too early, add a dummy mach if = .addMach(); != nil { return } = .G.AddEdge(, , graph.EdgeData()) if != nil { return } } _ = .Map.AddEdge(, ) } else { = .Properties.Data.(*EdgeData) } // debug // stateDbg := state // if isAdd { // stateDbg += " add" // } // if isPipeOut { // stateDbg += " out" // } // dump.Println(c.Id, stateDbg, sourceMachId+"->"+targetMachId, data) // update the missing state from the other side of the pipe := false for , := range .MachPipesTo { // IN if ! && .MutType == && .ToState == "" { .ToState = = true // DUP } else if ! && .MutType == && .ToState == { = true } // OUT if && .MutType == && .FromState == "" { .FromState = = true // DUP } else if && .MutType == && .FromState == { = true } if { break } } // add a new pipe to an existing edge if ! { := &MachPipeTo{ ToState: , MutType: , } if { = &MachPipeTo{ FromState: , MutType: , } } .MachPipesTo = append(.MachPipesTo, ) } // REMOVE PIPE } else if strings.HasPrefix(.Text, "[pipe:gc] ") { := strings.Split(.Text, " ") := [1] // TODO make it safe // outbound , := .G.AdjacencyMap() if != nil { return } for , := range [] { := .G.RemoveEdge(, .Target) if != nil { return } _ = .Map.RemoveEdge(, .Target) } // inbound , := .G.PredecessorMap() if != nil { return } for , := range [] { := .G.RemoveEdge(.Source, ) if != nil { return } _ = .Map.RemoveEdge(.Source, ) } } // TODO detached pipe handlers return nil } // FUNCS // Markdown returns a Markdown format of the network graph, with state lists, // with a flat hierarchy. It's useful for debugging. func ( map[string]*MachInspect) string { := slices.Collect(maps.Keys()) sort.Strings() := "# am-vis inspect-dump\n\n" for , := range { := [] += "## " + + "\n" // TODO format += fmt.Sprintf("Time: t%d\n", .Time) if .Child != "" { += fmt.Sprintf("Parent: %s\n\n", .Child) } else { += "\n" } if len(.States) > 0 { sort.Strings(.States) += "### States\n" += fmt.Sprintf("- %s\n", strings.Join(.States, "\n- ")) += "\n" } if len(.Conns) > 0 { sort.Strings(.Conns) += "### RPC\n" += fmt.Sprintf("- %s\n", strings.Join(.Conns, "\n- ")) += "\n" } if len(.Pipes) > 0 { += "### Pipes\n\n" := slices.Collect(maps.Keys(.Pipes)) sort.Strings() for , := range { sort.Strings(.Pipes[]) += fmt.Sprintf("#### %s\n", ) += fmt.Sprintf("- %s\n", strings.Join(.Pipes[], "\n- ")) += "\n" } } += "-----\n\n" } return } // Markup returns an XML format of the network graph, including full schemas, // with a nested hierarchy. It's useful for CSS/XPath queries. func ( map[string]*MachInspect) string { // build parent -> []children map and collect roots := make(map[string][]string) var []string for , := range { if .Child == "" { = append(, ) } else { [.Child] = append([.Child], ) } } sort.Strings() for := range { sort.Strings([]) } := "<graph>\n" for , := range { += "\n" += writeMachine(, , , 1) } += "</graph>\n" return } func writePipe(, , string) string { := := fmt.Sprintf(" to=%q", ) if strings.HasPrefix(, "[") { if := strings.Index(, "]"); > 1 { := strings.TrimSpace([1:]) := strings.TrimSpace([+1:]) if == "remove" { += fmt.Sprintf(" add=\"0\"") } else { += fmt.Sprintf(" add=\"1\"") } , , := strings.Cut(, " -> ") if { = strings.TrimSpace() = strings.TrimSpace() if != "" { += fmt.Sprintf(" as=%q", ) } switch { case != "": = case != "": = default: = } } else { = } } } return fmt.Sprintf("%s<pipe%s>%s</pipe>\n", , , ) } func writeMachine( map[string]*MachInspect, map[string][]string, string, int, ) string { // := [] := strings.Repeat(" ", ) := + " " := fmt.Sprintf("%s<machine id=%q time=\"%d\">\n", , , .Time) := slices.Clone(.States) sort.Strings() for , := range { += writeState(, , ) } := slices.Clone(.Conns) sort.Strings() for , := range { += fmt.Sprintf("%s<rpc>%s</rpc>\n", , ) } := slices.Collect(maps.Keys(.Pipes)) sort.Strings() for , := range { := slices.Clone(.Pipes[]) sort.Strings() for , := range { += writePipe(, , ) } } := slices.Clone(.Tags) sort.Strings() for , := range { += fmt.Sprintf("%s<tag>%s</tag>\n", , ) } for , := range [] { += "\n" += (, , , +1) } += + "</machine>\n" return } func writeState( string, *MachInspect, string) string { , := .Schema[] if ! { return fmt.Sprintf("%s<state>%s</state>\n", , ) } := "" if := slices.Index(.States, ); >= 0 && < len(.MTime) { += fmt.Sprintf(` tick="%d"`, .MTime[]) if am.IsActiveTick(.MTime[]) { += fmt.Sprintf(` active="1"`) } } if .Auto { += ` auto="1"` } if .Multi { += ` multi="1"` } := len(.Require) > 0 || len(.Add) > 0 || len(.Remove) > 0 || len(.After) > 0 if ! { return fmt.Sprintf("%s<state%s>%s</state>\n", , , ) } := fmt.Sprintf("%s<state%s>\n", , ) += fmt.Sprintf("%s %s\n", , ) := func( string, am.S) { if len() == 0 { return } := slices.Clone() sort.Strings() for , := range { += fmt.Sprintf("%s <%s>%s</%s>\n", , , , ) } } ("require", .Require) ("add", .Add) ("remove", .Remove) ("after", .After) += fmt.Sprintf("%s</state>\n", ) return }