Compare commits

..

2 Commits

Author SHA1 Message Date
douxu 9c9e652765 feat: merge refactor/topology-node-optimize into develop 2026-07-08 17:02:15 +08:00
douxu b05219ef4c refactor: replace topology tree with graph cache
- replace globalTree startup loading with globalTopologyGraph
- add topologyGraph adjacency cache with in/out edges, start nodes, and end nodes
- remove multiBranchTreeNode and legacy tree-building helpers
- make topology queries independent of the all-zero UUID virtual root
- keep TOPOLOGY_ANALYSIS on per-task DB querying while clarifying point-to-point reachability results
- add coverage for multi-parent topology reachability
2026-07-07 10:01:14 +08:00
51 changed files with 727 additions and 2628 deletions

View File

@ -4,8 +4,6 @@ package constants
const (
// DefaultScore define the default score for redissearch suggestion
DefaultScore = 1.0
// ComponentConfigKey define component config token used at token6
ComponentConfigKey = "component"
)
const (
@ -44,9 +42,6 @@ const (
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys"
// RedisSpecCompNSPathMeasSetKey define redis set key which store all measurement tag keys under specific component nspath
RedisSpecCompNSPathMeasSetKey = "%s_nspath_measurement_tag_keys"
)
const (

View File

@ -1,28 +0,0 @@
// Package database define database operation functions
package database
import (
"context"
"modelRT/orm"
"gorm.io/gorm"
)
// QueryComponentColumnNames returns all column names from the component table.
func QueryComponentColumnNames(ctx context.Context, db *gorm.DB) ([]string, error) {
columnTypes, err := db.WithContext(ctx).Migrator().ColumnTypes((&orm.Component{}).TableName())
if err != nil {
return nil, err
}
columnNames := make([]string, 0, len(columnTypes))
for _, columnType := range columnTypes {
columnName := columnType.Name()
if columnName == "" {
continue
}
columnNames = append(columnNames, columnName)
}
return columnNames, nil
}

View File

@ -28,7 +28,6 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
StationToCompNSPaths: make(map[string][]string),
CompNSPathToCompTags: make(map[string][]string),
CompTagToMeasTags: make(map[string][]string),
CompNSPathToMeasTags: make(map[string][]string),
}
g, gctx := errgroup.WithContext(ctx)
@ -114,14 +113,11 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
g.Go(func() error {
var measurements []struct {
orm.Measurement
CompTag string `gorm:"column:comp_tag"`
CompNSPath string `gorm:"column:comp_nspath"`
BayTag string `gorm:"column:bay_tag"`
CompTag string `gorm:"column:comp_tag"`
}
if err := db.Table("measurement").
Select("measurement.*, component.tag as comp_tag, component.nspath as comp_nspath, bay.tag as bay_tag").
Select("measurement.*, component.tag as comp_tag").
Joins("left join component on measurement.component_uuid = component.global_uuid").
Joins("left join bay on measurement.bay_uuid = bay.bay_uuid").
Scan(&measurements).Error; err != nil {
return fmt.Errorf("query measurements: %w", err)
}
@ -130,9 +126,6 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
if m.CompTag != "" {
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag)
}
if m.CompNSPath != "" && m.CompNSPath == m.BayTag {
mSet.CompNSPathToMeasTags[m.CompNSPath] = append(mSet.CompNSPathToMeasTags[m.CompNSPath], m.Tag)
}
}
return nil
})

View File

@ -3,11 +3,8 @@ package database
import (
"context"
"fmt"
"time"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/orm"
"modelRT/sql"
@ -24,16 +21,19 @@ func QueryTopologic(ctx context.Context, tx *gorm.DB) ([]orm.Topologic, error) {
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Raw(sql.RecursiveSQL, constants.UUIDNilStr).Scan(&topologics)
result := tx.WithContext(cancelCtx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Find(&topologics)
if result.Error != nil {
logger.Error(ctx, "query circuit diagram topologic info by start node uuid failed", "start_node_uuid", constants.UUIDNilStr, "error", result.Error)
logger.Error(ctx, "query circuit diagram topologic info failed", "error", result.Error)
return nil, result.Error
}
return topologics, nil
}
// QueryTopologicByStartUUID returns all edges reachable from startUUID following
// directed uuid_from → uuid_to edges in the topologic table.
// QueryTopologicByStartUUID returns all directed edges reachable from startUUID.
// It is used by point-to-point topology reachability checks and intentionally
// does not depend on the legacy all-zero UUID virtual root.
func QueryTopologicByStartUUID(ctx context.Context, tx *gorm.DB, startUUID uuid.UUID) ([]orm.Topologic, error) {
var topologics []orm.Topologic
@ -42,7 +42,7 @@ func QueryTopologicByStartUUID(ctx context.Context, tx *gorm.DB, startUUID uuid.
result := tx.WithContext(cancelCtx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Raw(sql.RecursiveSQL, startUUID).
Raw(sql.RecursiveTopologicByStartSQL, startUUID).
Scan(&topologics)
if result.Error != nil {
logger.Error(ctx, "query topologic by start uuid failed", "start_uuid", startUUID, "error", result.Error)
@ -50,80 +50,3 @@ func QueryTopologicByStartUUID(ctx context.Context, tx *gorm.DB, startUUID uuid.
}
return topologics, nil
}
// QueryTopologicFromDB return the result of query topologic info from DB.
// Returns the root node and a flat nodeMap for O(1) lookup by UUID.
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB) (*diagram.MultiBranchTreeNode, map[uuid.UUID]*diagram.MultiBranchTreeNode, error) {
topologicInfos, err := QueryTopologic(ctx, tx)
if err != nil {
logger.Error(ctx, "query topologic info failed", "error", err)
return nil, nil, err
}
tree, nodeMap, err := BuildMultiBranchTree(topologicInfos)
if err != nil {
logger.Error(ctx, "init topologic failed", "error", err)
return nil, nil, err
}
return tree, nodeMap, nil
}
// BuildMultiBranchTree return the multi branch tree by topologic info.
// Returns the root node and a flat nodeMap for O(1) lookup by UUID.
func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeNode, map[uuid.UUID]*diagram.MultiBranchTreeNode, error) {
nodeMap := make(map[uuid.UUID]*diagram.MultiBranchTreeNode, len(topologics)*2)
for _, topo := range topologics {
if _, exists := nodeMap[topo.UUIDFrom]; !exists {
// UUIDNil is the virtual root sentinel — skip creating a regular node for it
if topo.UUIDFrom != constants.UUIDNil {
nodeMap[topo.UUIDFrom] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDFrom,
Children: make([]*diagram.MultiBranchTreeNode, 0),
}
}
}
if _, exists := nodeMap[topo.UUIDTo]; !exists {
if topo.UUIDTo != constants.UUIDNil {
nodeMap[topo.UUIDTo] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDTo,
Children: make([]*diagram.MultiBranchTreeNode, 0),
}
}
}
}
for _, topo := range topologics {
var parent *diagram.MultiBranchTreeNode
if topo.UUIDFrom == constants.UUIDNil {
if _, exists := nodeMap[constants.UUIDNil]; !exists {
nodeMap[constants.UUIDNil] = &diagram.MultiBranchTreeNode{
ID: constants.UUIDNil,
Children: make([]*diagram.MultiBranchTreeNode, 0),
}
}
parent = nodeMap[constants.UUIDNil]
} else {
parent = nodeMap[topo.UUIDFrom]
}
var child *diagram.MultiBranchTreeNode
if topo.UUIDTo == constants.UUIDNil {
child = &diagram.MultiBranchTreeNode{
ID: topo.UUIDTo,
}
} else {
child = nodeMap[topo.UUIDTo]
}
child.Parent = parent
parent.Children = append(parent.Children, child)
}
// return root vertex
root, exists := nodeMap[constants.UUIDNil]
if !exists {
return nil, nil, fmt.Errorf("root node not found")
}
return root, nodeMap, nil
}

View File

@ -1,125 +0,0 @@
// Package diagram provide diagram data structure and operation
package diagram
import (
"fmt"
"github.com/gofrs/uuid"
)
var GlobalTree *MultiBranchTreeNode
// MultiBranchTreeNode represents a topological structure using an multi branch tree
type MultiBranchTreeNode struct {
ID uuid.UUID // 节点唯一标识
Parent *MultiBranchTreeNode // 指向父节点的指针
Children []*MultiBranchTreeNode // 指向所有子节点的指针切片
}
func NewMultiBranchTree(id uuid.UUID) *MultiBranchTreeNode {
return &MultiBranchTreeNode{
ID: id,
Children: make([]*MultiBranchTreeNode, 0),
}
}
func (n *MultiBranchTreeNode) AddChild(child *MultiBranchTreeNode) {
child.Parent = n
n.Children = append(n.Children, child)
}
func (n *MultiBranchTreeNode) RemoveChild(childID uuid.UUID) bool {
for i, child := range n.Children {
if child.ID == childID {
n.Children = append(n.Children[:i], n.Children[i+1:]...)
child.Parent = nil
return true
}
}
return false
}
func (n *MultiBranchTreeNode) FindNodeByID(id uuid.UUID) *MultiBranchTreeNode {
if n.ID == id {
return n
}
for _, child := range n.Children {
if found := child.FindNodeByID(id); found != nil {
return found
}
}
return nil
}
func (n *MultiBranchTreeNode) PrintTree(level int) {
for range level {
fmt.Print(" ")
}
fmt.Printf("-ID: %s\n", n.ID)
for _, child := range n.Children {
child.PrintTree(level + 1)
}
}
// FindPath returns the ordered node sequence from startID to endID using the
// supplied nodeMap for O(1) lookup. It walks each node up to the root to find
// the LCA, then stitches the two half-paths together.
// Returns nil when either node is absent from nodeMap or no path exists.
func FindPath(startID, endID uuid.UUID, nodeMap map[uuid.UUID]*MultiBranchTreeNode) []*MultiBranchTreeNode {
startNode, ok := nodeMap[startID]
if !ok {
return nil
}
endNode, ok := nodeMap[endID]
if !ok {
return nil
}
// collect ancestors (inclusive) from a node up to the root sentinel
ancestors := func(n *MultiBranchTreeNode) []*MultiBranchTreeNode {
var chain []*MultiBranchTreeNode
for n != nil {
chain = append(chain, n)
n = n.Parent
}
return chain
}
startChain := ancestors(startNode) // [start, ..., root]
endChain := ancestors(endNode) // [end, ..., root]
// index startChain by ID for fast LCA detection
startIdx := make(map[uuid.UUID]int, len(startChain))
for i, node := range startChain {
startIdx[node.ID] = i
}
// find LCA: first node in endChain that also appears in startChain
lcaEndPos := -1
lcaStartPos := -1
for i, node := range endChain {
if j, found := startIdx[node.ID]; found {
lcaEndPos = i
lcaStartPos = j
break
}
}
if lcaEndPos < 0 {
return nil // disconnected
}
// path = startChain[0..lcaStartPos] reversed + endChain[lcaEndPos..0] reversed
path := make([]*MultiBranchTreeNode, 0, lcaStartPos+lcaEndPos+1)
for i := 0; i <= lcaStartPos; i++ {
path = append(path, startChain[i])
}
// append end-side (skip LCA to avoid duplication), reversed
for i := lcaEndPos - 1; i >= 0; i-- {
path = append(path, endChain[i])
}
return path
}

138
diagram/topology_graph.go Normal file
View File

@ -0,0 +1,138 @@
package diagram
import (
"sync"
"modelRT/orm"
"github.com/gofrs/uuid"
)
// TopologyGraph represents directed topologic links with adjacency lists.
// It preserves multiple parents for one node.
type TopologyGraph struct {
Nodes map[uuid.UUID]struct{}
OutEdges map[uuid.UUID][]uuid.UUID
InEdges map[uuid.UUID][]uuid.UUID
StartNodes []uuid.UUID
EndNodes []uuid.UUID
}
var (
globalTopologyGraphMu sync.RWMutex
GlobalTopologyGraph *TopologyGraph
)
// NewTopologyGraph builds a directed graph cache from topologic edges.
func NewTopologyGraph(edges []orm.Topologic) *TopologyGraph {
graph := &TopologyGraph{
Nodes: make(map[uuid.UUID]struct{}, len(edges)*2),
OutEdges: make(map[uuid.UUID][]uuid.UUID, len(edges)),
InEdges: make(map[uuid.UUID][]uuid.UUID, len(edges)),
}
for _, edge := range edges {
from := edge.UUIDFrom
to := edge.UUIDTo
graph.Nodes[from] = struct{}{}
graph.Nodes[to] = struct{}{}
graph.OutEdges[from] = append(graph.OutEdges[from], to)
graph.InEdges[to] = append(graph.InEdges[to], from)
}
graph.StartNodes = graph.findStartNodes()
graph.EndNodes = graph.findEndNodes()
return graph
}
// SetGlobalTopologyGraph replaces the process-wide topology graph cache.
func SetGlobalTopologyGraph(graph *TopologyGraph) {
globalTopologyGraphMu.Lock()
defer globalTopologyGraphMu.Unlock()
GlobalTopologyGraph = graph
}
// GetGlobalTopologyGraph returns the process-wide topology graph cache.
func GetGlobalTopologyGraph() *TopologyGraph {
globalTopologyGraphMu.RLock()
defer globalTopologyGraphMu.RUnlock()
return GlobalTopologyGraph
}
func (g *TopologyGraph) findStartNodes() []uuid.UUID {
startNodes := make([]uuid.UUID, 0)
for id := range g.Nodes {
if len(g.InEdges[id]) == 0 && len(g.OutEdges[id]) > 0 {
startNodes = append(startNodes, id)
}
}
return startNodes
}
func (g *TopologyGraph) findEndNodes() []uuid.UUID {
endNodes := make([]uuid.UUID, 0)
for id := range g.Nodes {
if len(g.InEdges[id]) > 0 && len(g.OutEdges[id]) == 0 {
endNodes = append(endNodes, id)
}
}
return endNodes
}
// IsReachable reports whether end can be reached from start following directed
// uuid_from -> uuid_to edges.
func (g *TopologyGraph) IsReachable(start, end uuid.UUID) bool {
return len(g.FindPath(start, end)) > 0
}
// FindPath returns one shortest directed path from start to end, or nil when
// no directed path exists.
func (g *TopologyGraph) FindPath(start, end uuid.UUID) []uuid.UUID {
if g == nil {
return nil
}
if start == end {
if _, exists := g.Nodes[start]; exists {
return []uuid.UUID{start}
}
return nil
}
visited := map[uuid.UUID]struct{}{start: {}}
parent := make(map[uuid.UUID]uuid.UUID)
queue := []uuid.UUID{start}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, next := range g.OutEdges[cur] {
if _, seen := visited[next]; seen {
continue
}
visited[next] = struct{}{}
parent[next] = cur
if next == end {
return reconstructTopologyGraphPath(parent, start, end)
}
queue = append(queue, next)
}
}
return nil
}
func reconstructTopologyGraphPath(parent map[uuid.UUID]uuid.UUID, start, end uuid.UUID) []uuid.UUID {
path := make([]uuid.UUID, 0)
for cur := end; cur != start; cur = parent[cur] {
path = append(path, cur)
}
path = append(path, start)
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
return path
}

View File

@ -0,0 +1,35 @@
package diagram
import (
"testing"
"modelRT/orm"
"github.com/gofrs/uuid"
)
func TestTopologyGraphSupportsMultiParentReachability(t *testing.T) {
startA := uuid.Must(uuid.NewV4())
startB := uuid.Must(uuid.NewV4())
shared := uuid.Must(uuid.NewV4())
end := uuid.Must(uuid.NewV4())
graph := NewTopologyGraph([]orm.Topologic{
{UUIDFrom: startA, UUIDTo: shared},
{UUIDFrom: startB, UUIDTo: shared},
{UUIDFrom: shared, UUIDTo: end},
})
if len(graph.StartNodes) != 2 {
t.Fatalf("expected 2 start nodes, got %d", len(graph.StartNodes))
}
if len(graph.InEdges[shared]) != 2 {
t.Fatalf("expected shared node to keep 2 parents, got %d", len(graph.InEdges[shared]))
}
if !graph.IsReachable(startA, end) {
t.Fatalf("expected %s to reach %s", startA, end)
}
if !graph.IsReachable(startB, end) {
t.Fatalf("expected %s to reach %s", startB, end)
}
}

View File

@ -524,6 +524,10 @@ const docTemplate = `{
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
}
}
},

View File

@ -518,6 +518,10 @@
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
}
}
},

View File

@ -86,6 +86,9 @@ definitions:
items:
type: string
type: array
recommended_type:
example: grid_tag
type: string
type: object
network.RealTimeDataPayload:
properties:

View File

@ -15,14 +15,13 @@ import (
// QueryAlertEventHandler define query alert event process API
func QueryAlertEventHandler(c *gin.Context) {
ctx := c.Request.Context()
var targetLevel constants.AlertLevel
alertManger := alert.GetAlertMangerInstance()
levelStr := c.Query("level")
level, err := strconv.Atoi(levelStr)
if err != nil {
logger.Error(ctx, "convert alert level string to int failed", "error", err)
logger.Error(c, "convert alert level string to int failed", "error", err)
resp := network.FailureResponse{
Code: -1,

View File

@ -19,16 +19,15 @@ import (
// ComponentAnchorReplaceHandler define component anchor point replace process API
func ComponentAnchorReplaceHandler(c *gin.Context) {
ctx := c.Request.Context()
var uuid, anchorName string
pgClient := database.GetPostgresDBClient()
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
defer cancel()
var request network.ComponetAnchorReplaceRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "unmarshal component anchor point replace info failed", "error", err)
logger.Error(c, "unmarshal component anchor point replace info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -43,7 +42,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
var componentInfo orm.Component
result := pgClient.WithContext(cancelCtx).Model(&orm.Component{}).Where("global_uuid = ?", uuid).Find(&componentInfo)
if result.Error != nil {
logger.Error(ctx, "query component detail info failed", "error", result.Error)
logger.Error(c, "query component detail info failed", "error", result.Error)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -55,7 +54,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
if result.RowsAffected == 0 {
err := fmt.Errorf("query component detail info by uuid failed:%w", errcode.ErrQueryRowZero)
logger.Error(ctx, "query component detail info from table is empty", "table_name", "component")
logger.Error(c, "query component detail info from table is empty", "table_name", "component")
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -2,8 +2,6 @@
package handler
import (
"fmt"
"modelRT/constants"
"modelRT/database"
"modelRT/logger"
@ -154,8 +152,6 @@ func validateBatchImportParams(params map[string]any) bool {
func validateTestTaskParams(params map[string]any) bool {
// Test task has optional parameters, all are valid
// sleep_duration defaults to 60 seconds if not provided
// TODO Add more validation logic for test task parameters if needed
fmt.Println(params)
return true
}

View File

@ -13,13 +13,12 @@ import (
// AttrDeleteHandler deletes a data attribute
func AttrDeleteHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrDeleteRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -28,7 +27,7 @@ func AttrDeleteHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal attribute delete request", "error", err)
logger.Error(c, "failed to unmarshal attribute delete request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -36,9 +35,9 @@ func AttrDeleteHandler(c *gin.Context) {
return
}
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
if err := rs.GETDEL(request.AttrToken); err != nil {
logger.Error(ctx, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
logger.Error(c, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -13,14 +13,13 @@ import (
// AttrGetHandler retrieves the value of a data attribute
func AttrGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -29,7 +28,7 @@ func AttrGetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal attribute get request", "error", err)
logger.Error(c, "failed to unmarshal attribute get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -38,12 +37,12 @@ func AttrGetHandler(c *gin.Context) {
}
pgClient := database.GetPostgresDBClient()
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.Begin()
attrModel, err := database.ParseAttrToken(ctx, tx, request.AttrToken, clientToken)
attrModel, err := database.ParseAttrToken(c, tx, request.AttrToken, clientToken)
if err != nil {
tx.Rollback()
logger.Error(ctx, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
logger.Error(c, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -13,14 +13,13 @@ import (
// AttrSetHandler sets the value of a data attribute
func AttrSetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrSetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -29,7 +28,7 @@ func AttrSetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal attribute set request", "error", err)
logger.Error(c, "failed to unmarshal attribute set request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -38,9 +37,9 @@ func AttrSetHandler(c *gin.Context) {
}
// The logic for handling Redis operations directly from the handler
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
if err := rs.Set(request.AttrToken, request.AttrValue); err != nil {
logger.Error(ctx, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
logger.Error(c, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -16,12 +16,11 @@ import (
// CircuitDiagramCreateHandler define circuit diagram create process API
func CircuitDiagramCreateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "unmarshal circuit diagram create info failed", "error", err)
logger.Error(c, "unmarshal circuit diagram create info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -33,7 +32,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -61,7 +60,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
}
logger.Error(ctx, "format uuid from string failed", "error", err)
logger.Error(c, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -79,13 +78,13 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.Begin()
err = database.CreateTopologicIntoDB(ctx, tx, request.PageID, topologicCreateInfos)
err = database.CreateTopologicIntoDB(c, tx, request.PageID, topologicCreateInfos)
if err != nil {
tx.Rollback()
logger.Error(ctx, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
logger.Error(c, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -103,11 +102,11 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
}
for index, info := range request.ComponentInfos {
componentUUID, err := database.CreateComponentIntoDB(ctx, tx, info)
componentUUID, err := database.CreateComponentIntoDB(c, tx, info)
if err != nil {
tx.Rollback()
logger.Error(ctx, "insert component info into DB failed", "error", err)
logger.Error(c, "insert component info into DB failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -126,7 +125,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
// TODO 修复赋值问题
component, err := network.ConvertComponentCreateInfosToComponents(info)
if err != nil {
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -21,12 +21,11 @@ import (
// CircuitDiagramDeleteHandler define circuit diagram delete process API
func CircuitDiagramDeleteHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramDeleteRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "unmarshal circuit diagram del info failed", "error", err)
logger.Error(c, "unmarshal circuit diagram del info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -38,7 +37,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -66,7 +65,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
}
logger.Error(ctx, "format uuid from string failed", "error", err)
logger.Error(c, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -84,14 +83,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.Begin()
for _, topologicDelInfo := range topologicDelInfos {
err = database.DeleteTopologicIntoDB(ctx, tx, request.PageID, topologicDelInfo)
err = database.DeleteTopologicIntoDB(c, tx, request.PageID, topologicDelInfo)
if err != nil {
tx.Rollback()
logger.Error(ctx, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
logger.Error(c, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -108,7 +107,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
if err != nil {
tx.Rollback()
logger.Error(ctx, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
logger.Error(c, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -127,14 +126,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
}
for _, componentInfo := range request.ComponentInfos {
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
defer cancel()
globalUUID, err := uuid.FromString(componentInfo.UUID)
if err != nil {
tx.Rollback()
logger.Error(ctx, "format uuid from string failed", "error", err)
logger.Error(c, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -158,7 +157,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
}
logger.Error(ctx, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
logger.Error(c, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -180,7 +179,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
}
logger.Error(ctx, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
logger.Error(c, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -24,12 +24,11 @@ import (
// @Failure 400 {object} network.FailureResponse "request process failed"
// @Router /model/diagram_load/{page_id} [get]
func CircuitDiagramLoadHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
pageID, err := strconv.ParseInt(c.Query("page_id"), 10, 64)
if err != nil {
logger.Error(ctx, "get pageID from url param failed", "error", err)
logger.Error(c, "get pageID from url param failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -44,7 +43,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
topologicInfo, err := diagram.GetGraphMap(pageID)
if err != nil {
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -63,9 +62,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
componentParamMap := make(map[string]any)
for _, VerticeLink := range topologicInfo.VerticeLinks {
for _, componentUUID := range VerticeLink {
component, err := database.QueryComponentByUUID(ctx, pgClient, componentUUID)
component, err := database.QueryComponentByUUID(c, pgClient, componentUUID)
if err != nil {
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -80,7 +79,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
componentParams, err := diagram.GetComponentMap(component.GlobalUUID.String())
if err != nil {
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
logger.Error(c, "get component data from set by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -97,9 +96,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
}
rootVertexUUID := topologicInfo.RootVertex.String()
rootComponent, err := database.QueryComponentByUUID(ctx, pgClient, topologicInfo.RootVertex)
rootComponent, err := database.QueryComponentByUUID(c, pgClient, topologicInfo.RootVertex)
if err != nil {
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -114,7 +113,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
rootComponentParam, err := diagram.GetComponentMap(rootComponent.GlobalUUID.String())
if err != nil {
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
logger.Error(c, "get component data from set by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -14,12 +14,11 @@ import (
// CircuitDiagramUpdateHandler define circuit diagram update process API
func CircuitDiagramUpdateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramUpdateRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "unmarshal circuit diagram update info failed", "error", err)
logger.Error(c, "unmarshal circuit diagram update info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -31,7 +30,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -48,7 +47,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
for _, topologicLink := range request.TopologicLinks {
changeInfo, err := network.ParseUUID(topologicLink)
if err != nil {
logger.Error(ctx, "format uuid from string failed", "error", err)
logger.Error(c, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -64,14 +63,14 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.Begin()
for _, topologicChangeInfo := range topologicChangeInfos {
err = database.UpdateTopologicIntoDB(ctx, tx, request.PageID, topologicChangeInfo)
err = database.UpdateTopologicIntoDB(c, tx, request.PageID, topologicChangeInfo)
if err != nil {
tx.Rollback()
logger.Error(ctx, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
logger.Error(c, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -88,7 +87,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
if err != nil {
tx.Rollback()
logger.Error(ctx, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
logger.Error(c, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -103,9 +102,9 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
}
for index, componentInfo := range request.ComponentInfos {
componentUUID, err := database.UpdateComponentIntoDB(ctx, tx, componentInfo)
componentUUID, err := database.UpdateComponentIntoDB(c, tx, componentInfo)
if err != nil {
logger.Error(ctx, "udpate component info into DB failed", "error", err)
logger.Error(c, "udpate component info into DB failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -125,7 +124,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
// TODO 修复赋值问题
component, err := network.ConvertComponentUpdateInfosToComponents(info)
if err != nil {
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -22,13 +22,12 @@ import (
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
func ComponentAttributeQueryHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
tokens := c.Param("tokens")
if tokens == "" {
err := fmt.Errorf("tokens is missing from the path")
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
logger.Error(c, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -55,10 +54,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
dbQueryMap := make(map[string][]cacheQueryItem)
var secondaryQueryCount int
for hSetKey, items := range cacheQueryMap {
hset := diagram.NewRedisHash(ctx, hSetKey, 5000, false)
hset := diagram.NewRedisHash(c, hSetKey, 5000, false)
cacheData, err := hset.HGetAll()
if err != nil {
logger.Warn(ctx, "redis hgetall failed", "key", hSetKey, "err", err)
logger.Warn(c, "redis hgetall failed", "key", hSetKey, "err", err)
}
for _, item := range items {
if val, ok := cacheData[item.attributeName]; ok {
@ -76,9 +75,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
return
}
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.WithContext(c).Begin()
if tx.Error != nil {
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrBeginTxFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload)
@ -87,9 +86,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
defer tx.Rollback()
allCompTags := slices.Collect(maps.Keys(dbQueryMap))
compModelMap, err := database.QueryComponentByCompTags(ctx, tx, allCompTags)
compModelMap, err := database.QueryComponentByCompTags(c, tx, allCompTags)
if err != nil {
logger.Error(ctx, "query component info from postgres database failed", "error", err)
logger.Error(c, "query component info from postgres database failed", "error", err)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrDBQueryFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload)
@ -117,7 +116,7 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
if err != nil {
logger.Error(ctx, "batch get table names from postgres database failed", "error", err)
logger.Error(c, "batch get table names from postgres database failed", "error", err)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrRetrieveFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload)
@ -152,11 +151,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
}
if err := tx.Commit().Error; err != nil {
logger.Warn(ctx, "postgres transaction commit failed, but returning scanned data", "error", err)
logger.Warn(c, "postgres transaction commit failed, but returning scanned data", "error", err)
} else {
backfillCtx := context.WithoutCancel(ctx)
for hKey, items := range redisSyncMap {
go backfillRedis(backfillCtx, hKey, items)
go backfillRedis(c.Copy(), hKey, items)
}
}

View File

@ -18,11 +18,10 @@ import (
// ComponentAttributeUpdateHandler define circuit diagram component attribute value update process API
func ComponentAttributeUpdateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.ComponentAttributeUpdateInfo
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "unmarshal request params failed", "error", err)
logger.Error(c, "unmarshal request params failed", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -55,16 +54,16 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.WithContext(ctx).Begin()
tx := pgClient.WithContext(c).Begin()
if tx.Error != nil {
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
return
}
compInfo, err := database.QueryComponentByCompTag(ctx, tx, attributeComponentTag)
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag)
if err != nil {
logger.Error(ctx, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
logger.Error(c, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
for _, attribute := range request.AttributeConfigs {
if _, exists := updateResults[attribute.AttributeToken]; !exists {
@ -140,7 +139,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
for key, items := range redisUpdateMap {
hset := diagram.NewRedisHash(ctx, key, 5000, false)
hset := diagram.NewRedisHash(c, key, 5000, false)
fields := make(map[string]any, len(items))
for _, item := range items {
@ -148,7 +147,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
if err := hset.SetRedisHashByMap(fields); err != nil {
logger.Error(ctx, "batch sync redis failed", "hash_key", key, "error", err)
logger.Error(c, "batch sync redis failed", "hash_key", key, "error", err)
for _, item := range items {
if _, exists := updateResults[item.token]; exists {

View File

@ -41,12 +41,11 @@ var linkSetConfigs = map[int]linkSetConfig{
// DiagramNodeLinkHandler defines the diagram node link process api
func DiagramNodeLinkHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.DiagramNodeLinkRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -55,7 +54,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal diagram node process request", "error", err)
logger.Error(c, "failed to unmarshal diagram node process request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -69,9 +68,9 @@ func DiagramNodeLinkHandler(c *gin.Context) {
nodeID := request.NodeID
nodeLevel := request.NodeLevel
action := request.Action
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(ctx, pgClient, nodeID, nodeLevel)
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(c, pgClient, nodeID, nodeLevel)
if err != nil {
logger.Error(ctx, "failed to query diagram node info by nodeID and level from postgres", "node_id", nodeID, "level", nodeLevel, "error", err)
logger.Error(c, "failed to query diagram node info by nodeID and level from postgres", "node_id", nodeID, "level", nodeLevel, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -85,8 +84,8 @@ func DiagramNodeLinkHandler(c *gin.Context) {
return
}
prevLinkSet, currLinkSet := generateLinkSet(ctx, nodeLevel, prevNodeInfo)
err = processLinkSetData(ctx, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
prevLinkSet, currLinkSet := generateLinkSet(c, nodeLevel, prevNodeInfo)
err = processLinkSetData(c, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
if err != nil {
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -100,7 +99,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
return
}
logger.Info(ctx, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
logger.Info(c, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,

View File

@ -16,12 +16,11 @@ import (
// QueryHistoryDataHandler define query history data process API
func QueryHistoryDataHandler(c *gin.Context) {
ctx := c.Request.Context()
token := c.Query("token")
beginStr := c.Query("begin")
begin, err := strconv.Atoi(beginStr)
if err != nil {
logger.Error(ctx, "convert begin param from string to int failed", "error", err)
logger.Error(c, "convert begin param from string to int failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -32,7 +31,7 @@ func QueryHistoryDataHandler(c *gin.Context) {
endStr := c.Query("end")
end, err := strconv.Atoi(endStr)
if err != nil {
logger.Error(ctx, "convert end param from string to int failed", "error", err)
logger.Error(c, "convert end param from string to int failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -15,14 +15,13 @@ import (
// MeasurementGetHandler define measurement query API
func MeasurementGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -31,7 +30,7 @@ func MeasurementGetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal measurement get request", "error", err)
logger.Error(c, "failed to unmarshal measurement get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -39,10 +38,10 @@ func MeasurementGetHandler(c *gin.Context) {
return
}
zset := diagram.NewRedisZSet(ctx, request.MeasurementToken, 0, false)
zset := diagram.NewRedisZSet(c, request.MeasurementToken, 0, false)
points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
if err != nil {
logger.Error(ctx, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
logger.Error(c, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
@ -55,9 +54,9 @@ func MeasurementGetHandler(c *gin.Context) {
}
pgClient := database.GetPostgresDBClient()
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, request.MeasurementID)
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, request.MeasurementID)
if err != nil {
logger.Error(ctx, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
logger.Error(c, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -2,13 +2,11 @@
package handler
import (
"errors"
"strings"
"modelRT/constants"
"modelRT/logger"
"modelRT/model"
"modelRT/network"
"modelRT/util"
"net/http"
"github.com/gin-gonic/gin"
)
@ -45,81 +43,79 @@ import (
//
// @Router /measurement/recommend [get]
func MeasurementRecommendHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementRecommendRequest
if err := c.ShouldBindQuery(&request); err != nil {
logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
if err := validateMeasurementRecommendInput(request.Input); err != nil {
logger.Warn(ctx, "invalid measurement recommend input", "input", request.Input, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{
"input": request.Input,
logger.Error(c, "failed to bind measurement recommend request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
recommendResults := model.RedisSearchRecommend(ctx, request.Input)
payload := network.MeasurementRecommendPayload{
Input: request.Input,
RecommendedList: make([]string, 0),
}
seen := make(map[string]struct{})
orderedResults := orderedRecommendResults(recommendResults)
for _, recommendResult := range orderedResults {
recommendResults := model.RedisSearchRecommend(c, request.Input)
payloads := make([]network.MeasurementRecommendPayload, 0, len(recommendResults))
for _, recommendResult := range recommendResults {
if recommendResult.Err != nil {
err := recommendResult.Err
logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
renderRespFailure(c, constants.RespCodeServerError, err.Error(), map[string]any{
"input": request.Input,
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
Payload: map[string]any{
"input": request.Input,
},
})
return
}
if recommendResult.Offset > payload.Offset {
payload.Offset = recommendResult.Offset
var finalOffset int
recommends := recommendResult.QueryDatas
if recommendResult.IsFuzzy {
var maxOffset int
for index, recommend := range recommends {
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset > maxOffset {
maxOffset = offset
}
}
finalOffset = maxOffset
} else {
var minOffset int
for index, recommend := range recommends {
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset < minOffset {
minOffset = offset
}
}
finalOffset = minOffset
}
}
for _, recommendResult := range orderedResults {
for _, recommend := range recommendResult.QueryDatas {
if _, exists := seen[recommend]; !exists {
seen[recommend] = struct{}{}
payload.RecommendedList = append(payload.RecommendedList, recommend)
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:]
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
}
}
}
payloads = append(payloads, network.MeasurementRecommendPayload{
Input: request.Input,
Offset: finalOffset,
RecommendType: recommendResult.RecommendType.String(),
RecommendedList: resultRecommends,
})
}
renderRespSuccess(c, constants.RespCodeSuccess, "success", payload)
}
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
orderedTypes := []string{
constants.CompNSPathRecommendHierarchyType.String(),
constants.GridRecommendHierarchyType.String(),
constants.ZoneRecommendHierarchyType.String(),
constants.StationRecommendHierarchyType.String(),
constants.CompTagRecommendHierarchyType.String(),
constants.ConfigRecommendHierarchyType.String(),
constants.MeasTagRecommendHierarchyType.String(),
}
results := make([]model.SearchResult, 0, len(recommendResults))
for _, recommendType := range orderedTypes {
result, ok := recommendResults[recommendType]
if !ok {
continue
}
results = append(results, result)
}
return results
}
func validateMeasurementRecommendInput(input string) error {
if strings.Contains(input, "..") {
return errors.New("input contains continuous dots")
}
return nil
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
Payload: &payloads,
})
}

View File

@ -1,44 +0,0 @@
package handler
import "testing"
func TestValidateMeasurementRecommendInput(t *testing.T) {
tests := []struct {
name string
input string
valid bool
}{
{
name: "continuous dots",
input: "G..",
valid: false,
},
{
name: "single separator",
input: "G.zone",
valid: true,
},
{
name: "trailing dot",
input: "G.",
valid: true,
},
{
name: "empty input",
input: "",
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateMeasurementRecommendInput(tt.input)
if tt.valid && err != nil {
t.Fatalf("expected valid input, got error %v", err)
}
if !tt.valid && err == nil {
t.Fatalf("expected invalid input")
}
})
}
}

View File

@ -18,12 +18,11 @@ import (
// MeasurementLinkHandler defines the measurement link process api
func MeasurementLinkHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementLinkRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(ctx, "failed to get client token from context", "error", err)
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -32,7 +31,7 @@ func MeasurementLinkHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal measurement process request", "error", err)
logger.Error(c, "failed to unmarshal measurement process request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -45,9 +44,9 @@ func MeasurementLinkHandler(c *gin.Context) {
pgClient := database.GetPostgresDBClient()
measurementID := request.MeasurementID
action := request.Action
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, measurementID)
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, measurementID)
if err != nil {
logger.Error(ctx, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
logger.Error(c, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -60,9 +59,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
componentInfo, err := database.QueryComponentByUUID(ctx, pgClient, measurementInfo.ComponentUUID)
componentInfo, err := database.QueryComponentByUUID(c, pgClient, measurementInfo.ComponentUUID)
if err != nil {
logger.Error(ctx, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
logger.Error(c, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -75,9 +74,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
allMeasSet := diagram.NewRedisSet(ctx, constants.RedisAllMeasTagSetKey, 0, false)
allMeasSet := diagram.NewRedisSet(c, constants.RedisAllMeasTagSetKey, 0, false)
compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag)
compMeasLinkSet := diagram.NewRedisSet(ctx, compMeasLinkKey, 0, false)
compMeasLinkSet := diagram.NewRedisSet(c, compMeasLinkKey, 0, false)
switch action {
case constants.SearchLinkAddAction:
@ -85,18 +84,18 @@ func MeasurementLinkHandler(c *gin.Context) {
err2 := compMeasLinkSet.SADD(measurementInfo.Tag)
err = processActionError(err1, err2, action)
if err != nil {
logger.Error(ctx, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(c, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
}
case constants.SearchLinkDelAction:
err1 := allMeasSet.SREM(measurementInfo.Tag)
err2 := compMeasLinkSet.SREM(measurementInfo.Tag)
err = processActionError(err1, err2, action)
if err != nil {
logger.Error(ctx, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(c, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
}
default:
err = common.ErrUnsupportedLinkAction
logger.Error(ctx, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(c, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
}
if err != nil {
@ -111,7 +110,7 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
logger.Info(ctx, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
logger.Info(c, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,

View File

@ -35,28 +35,27 @@ var pullUpgrader = websocket.Upgrader{
// @Tags RealTime Component Websocket
// @Router /monitors/data/realtime/stream/:clientID [get]
func PullRealTimeDataHandler(c *gin.Context) {
requestCtx := c.Request.Context()
clientID := c.Param("clientID")
if clientID == "" {
err := fmt.Errorf("clientID is missing from the path")
logger.Error(requestCtx, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
logger.Error(c, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
renderWSRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
conn, err := pullUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(requestCtx, "upgrade http protocol to websocket protocol failed", "error", err)
logger.Error(c, "upgrade http protocol to websocket protocol failed", "error", err)
renderWSRespFailure(c, constants.RespCodeServerError, err.Error(), nil)
return
}
defer conn.Close()
ctx, cancel := context.WithCancel(requestCtx)
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
conn.SetCloseHandler(func(code int, text string) error {
logger.Info(requestCtx, "websocket processor shutdown trigger",
logger.Info(c.Request.Context(), "websocket processor shutdown trigger",
"clientID", clientID, "code", code, "reason", text)
// call cancel to notify other goroutines to stop working

View File

@ -60,11 +60,10 @@ var wsUpgrader = websocket.Upgrader{
//
// @Router /data/realtime [get]
func QueryRealTimeDataHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.RealTimeQueryRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
logger.Error(c, "failed to unmarshal real time query request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -74,7 +73,7 @@ func QueryRealTimeDataHandler(c *gin.Context) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
return
}
defer conn.Close()
@ -88,29 +87,29 @@ func QueryRealTimeDataHandler(c *gin.Context) {
case data := <-transportChannel:
respByte, err := jsoniter.Marshal(data)
if err != nil {
logger.Error(ctx, "marshal real time data to bytes failed", "error", err)
logger.Error(c, "marshal real time data to bytes failed", "error", err)
continue
}
err = conn.WriteMessage(websocket.TextMessage, respByte)
if err != nil {
logger.Error(ctx, "write message to websocket connection failed", "error", err)
logger.Error(c, "write message to websocket connection failed", "error", err)
continue
}
case <-closeChannel:
logger.Info(ctx, "data receiving goroutine has been closed")
logger.Info(c, "data receiving goroutine has been closed")
// TODO 优化时间控制
deadline := time.Now().Add(5 * time.Second)
err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline)
if err != nil {
logger.Error(ctx, "sending close control message failed", "error", err)
logger.Error(c, "sending close control message failed", "error", err)
}
// gracefully close session processing
err = conn.Close()
if err != nil {
logger.Error(ctx, "websocket conn closed failed", "error", err)
logger.Error(c, "websocket conn closed failed", "error", err)
}
logger.Info(ctx, "websocket connection closed successfully.")
logger.Info(c, "websocket connection closed successfully.")
}
}
}

View File

@ -17,10 +17,9 @@ var upgrader = websocket.Upgrader{
// RealTimeDataReceivehandler define real time data receive and process API
func RealTimeDataReceivehandler(c *gin.Context) {
ctx := c.Request.Context()
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
return
}
defer conn.Close()
@ -28,17 +27,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
logger.Error(ctx, "read message from websocket connection failed", "error", err)
logger.Error(c, "read message from websocket connection failed", "error", err)
respByte := processResponse(-1, "read message from websocket connection failed", nil)
if len(respByte) == 0 {
logger.Error(ctx, "process message from byte failed", "error", err)
logger.Error(c, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(ctx, "write message to websocket connection failed", "error", err)
logger.Error(c, "write message to websocket connection failed", "error", err)
continue
}
continue
@ -47,17 +46,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
var request network.RealTimeDataReceiveRequest
err = jsoniter.Unmarshal([]byte(p), &request)
if err != nil {
logger.Error(ctx, "unmarshal message from byte failed", "error", err)
logger.Error(c, "unmarshal message from byte failed", "error", err)
respByte := processResponse(-1, "unmarshal message from byte failed", nil)
if len(respByte) == 0 {
logger.Error(ctx, "process message from byte failed", "error", err)
logger.Error(c, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(ctx, "write message to websocket connection failed", "error", err)
logger.Error(c, "write message to websocket connection failed", "error", err)
continue
}
continue
@ -71,13 +70,13 @@ func RealTimeDataReceivehandler(c *gin.Context) {
}
respByte := processResponse(0, "success", payload)
if len(respByte) == 0 {
logger.Error(ctx, "process message from byte failed", "error", err)
logger.Error(c, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(ctx, "write message to websocket connection failed", "error", err)
logger.Error(c, "write message to websocket connection failed", "error", err)
continue
}
}

View File

@ -77,13 +77,12 @@ func init() {
//
// @Router /monitors/data/subscriptions [post]
func RealTimeSubHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.RealTimeSubRequest
var subAction string
var clientID string
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
logger.Error(c, "failed to unmarshal real time query request", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -92,7 +91,7 @@ func RealTimeSubHandler(c *gin.Context) {
subAction = request.Action
id, err := uuid.NewV4()
if err != nil {
logger.Error(ctx, "failed to generate client id", "error", err)
logger.Error(c, "failed to generate client id", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -115,9 +114,9 @@ func RealTimeSubHandler(c *gin.Context) {
switch subAction {
case constants.SubStartAction:
results, err := globalSubState.CreateConfig(ctx, tx, clientID, request.Measurements)
results, err := globalSubState.CreateConfig(c, tx, clientID, request.Measurements)
if err != nil {
logger.Error(ctx, "create real time data subscription config failed", "error", err)
logger.Error(c, "create real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -131,9 +130,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubStopAction:
results, err := globalSubState.RemoveTargets(ctx, clientID, request.Measurements)
results, err := globalSubState.RemoveTargets(c, clientID, request.Measurements)
if err != nil {
logger.Error(ctx, "remove target to real time data subscription config failed", "error", err)
logger.Error(c, "remove target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -147,9 +146,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubAppendAction:
results, err := globalSubState.AppendTargets(ctx, tx, clientID, request.Measurements)
results, err := globalSubState.AppendTargets(c, tx, clientID, request.Measurements)
if err != nil {
logger.Error(ctx, "append target to real time data subscription config failed", "error", err)
logger.Error(c, "append target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -163,9 +162,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubUpdateAction:
results, err := globalSubState.UpdateTargets(ctx, tx, clientID, request.Measurements)
results, err := globalSubState.UpdateTargets(c, tx, clientID, request.Measurements)
if err != nil {
logger.Error(ctx, "update target to real time data subscription config failed", "error", err)
logger.Error(c, "update target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -180,7 +179,7 @@ func RealTimeSubHandler(c *gin.Context) {
return
default:
err := fmt.Errorf("%w: request action is %s", common.ErrUnsupportedSubAction, request.Action)
logger.Error(ctx, "unsupported action of real time data subscription request", "error", err)
logger.Error(c, "unsupported action of real time data subscription request", "error", err)
requestTargetsCount := processRealTimeRequestCount(request.Measurements)
results := processRealTimeRequestTargets(request.Measurements, requestTargetsCount, constants.CodeUnsupportSubOperation, err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), network.RealTimeSubPayload{

View File

@ -18,8 +18,6 @@ type facade struct {
_logger *zap.Logger
}
const facadeCallerSkip = 2
// Debug define facade func of debug level log
func Debug(ctx context.Context, msg string, kv ...any) {
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
@ -41,16 +39,12 @@ func Error(ctx context.Context, msg string, kv ...any) {
}
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
f.logSkip(ctx, lvl, 1, msg, kv...)
f.logSkip(ctx, lvl, 0, msg, kv...)
}
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
fields := makeLogFieldsSkip(ctx, extraSkip, kv...)
logger := f._logger
if extraSkip > 0 {
logger = logger.WithOptions(zap.AddCallerSkip(extraSkip))
}
ce := logger.Check(lvl, msg)
ce := f._logger.Check(lvl, msg)
ce.Write(fields...)
}
@ -72,7 +66,7 @@ func InfoSkip(ctx context.Context, extraSkip int, msg string, kv ...any) {
func logFacade() *facade {
fOnce.Do(func() {
f = &facade{
_logger: GetLoggerInstance().WithOptions(zap.AddCallerSkip(facadeCallerSkip)),
_logger: GetLoggerInstance(),
}
})
return f

16
main.go
View File

@ -235,18 +235,6 @@ func main() {
panic(err)
}
componentColumnNames, err := database.QueryComponentColumnNames(ctx, tx)
if err != nil {
logger.Error(ctx, "query component table column names failed", "error", err)
panic(err)
}
err = model.StoreComponentColumnRecommend(ctx, fullParentPath, isLocalParentPath, componentColumnNames)
if err != nil {
logger.Error(ctx, "store component column recommend content failed", "error", err)
panic(err)
}
allMeasurement, err := database.GetAllMeasurements(ctx, tx)
if err != nil {
logger.Error(ctx, "load topologic info from postgres failed", "error", err)
@ -254,12 +242,12 @@ func main() {
}
go realtimedata.StartComputingRealTimeDataLimit(ctx, allMeasurement)
tree, _, err := database.QueryTopologicFromDB(ctx, tx)
topologics, err := database.QueryTopologic(ctx, tx)
if err != nil {
logger.Error(ctx, "load topologic info from postgres failed", "error", err)
panic(err)
}
diagram.GlobalTree = tree
diagram.SetGlobalTopologyGraph(diagram.NewTopologyGraph(topologics))
return nil
})

View File

@ -12,7 +12,6 @@ import (
"modelRT/orm"
"github.com/RediSearch/redisearch-go/v2/redisearch"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
)
@ -21,13 +20,6 @@ type columnParam struct {
AttributeGroup map[string]any
}
type attributeGroupRecommendJob struct {
AttributeSet orm.AttributeSet
FullPath string
IsLocalFullPath string
ColumnParam columnParam
}
// TraverseAttributeGroupTables define func to traverse component attribute group tables
func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFullPath map[string]string, isLocalCompTagToFullPath map[string]string, compAttrSet map[string]orm.AttributeSet) error {
var tableNames []string
@ -46,7 +38,6 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
return nil
}
jobs := make([]attributeGroupRecommendJob, 0)
for _, tableName := range tableNames {
var records []map[string]any
err := db.Table(tableName).Find(&records).Error
@ -111,27 +102,13 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
AttributeType: attributeType,
AttributeGroup: attributeGroup,
}
jobs = append(jobs, attributeGroupRecommendJob{
AttributeSet: attrSet,
FullPath: fullPath,
IsLocalFullPath: isLocalfullPath,
ColumnParam: columnParam,
})
go storeAttributeGroup(ctx, attrSet, fullPath, isLocalfullPath, columnParam)
}
}
group, groupCtx := errgroup.WithContext(ctx)
group.SetLimit(16)
for _, job := range jobs {
job := job
group.Go(func() error {
return storeAttributeGroup(groupCtx, job.AttributeSet, job.FullPath, job.IsLocalFullPath, job.ColumnParam)
})
}
return group.Wait()
return nil
}
func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, fullPath string, isLocalFullPath string, colParams columnParam) error {
func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, fullPath string, isLocalFullPath string, colParams columnParam) {
rdb := diagram.GetRedisClientInstance()
pipe := rdb.Pipeline()
@ -144,25 +121,18 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
attrbutesGroups := make([]any, 0, len(colParams.AttributeGroup)*2)
attributeGroupKey := fmt.Sprintf("%s_%s", attributeSet.CompTag, colParams.AttributeType)
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*2+2)
configTerm := fmt.Sprintf("%s.%s", fullPath, colParams.AttributeType)
sug = append(sug, redisearch.Suggestion{
Term: configTerm,
Score: constants.DefaultScore,
})
if isLocalFullPath != "" {
configTerm = fmt.Sprintf("%s.%s", isLocalFullPath, colParams.AttributeType)
sug = append(sug, redisearch.Suggestion{
Term: configTerm,
Score: constants.DefaultScore,
})
}
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*4)
for attrName, attrValue := range colParams.AttributeGroup {
attrbutesGroups = append(attrbutesGroups, attrName, attrValue)
attrNameMembers = append(attrNameMembers, attrName)
// add redis fuzzy search suggestion for token1-token7 type
configTerm := fmt.Sprintf("%s.%s", fullPath, colParams.AttributeType)
sug = append(sug, redisearch.Suggestion{
Term: configTerm,
Score: constants.DefaultScore,
})
measTerm := fmt.Sprintf("%s.%s.%s", fullPath, colParams.AttributeType, attrName)
sug = append(sug, redisearch.Suggestion{
Term: measTerm,
@ -170,9 +140,11 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
})
// add redis fuzzy search suggestion for token4-token7 type
if isLocalFullPath == "" {
continue
}
configTerm = fmt.Sprintf("%s.%s", isLocalFullPath, colParams.AttributeType)
sug = append(sug, redisearch.Suggestion{
Term: configTerm,
Score: constants.DefaultScore,
})
measTerm = fmt.Sprintf("%s.%s.%s", isLocalFullPath, colParams.AttributeType, attrName)
sug = append(sug, redisearch.Suggestion{
@ -191,24 +163,11 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
}
if len(sug) > 0 {
if err := ac.AddTerms(sug...); err != nil {
logger.Error(ctx, "add attribute group recommend suggestions failed",
"component_tag", attributeSet.CompTag,
"attribute_type", colParams.AttributeType,
"error", err,
)
return fmt.Errorf("add attribute group recommend suggestions: %w", err)
}
ac.AddTerms(sug...)
}
_, err := pipe.Exec(ctx)
if err != nil {
logger.Error(ctx, "init component attribute group recommend content failed",
"component_tag", attributeSet.CompTag,
"attribute_type", colParams.AttributeType,
"error", err,
)
return fmt.Errorf("init component attribute group recommend content: %w", err)
logger.Error(ctx, "init component attribute group recommend content failed", "error", err)
}
return nil
}

View File

@ -90,6 +90,6 @@ func (s *ShortAttrInfo) IsLocal() bool {
}
// GetAttrValue define return the attribute value
func (s *ShortAttrInfo) GetAttrValue() any {
return s.AttrValue
func (l *ShortAttrInfo) GetAttrValue() any {
return l.AttrValue
}

View File

@ -1,91 +0,0 @@
// Package model define model struct of model runtime service
package model
import (
"context"
"fmt"
"modelRT/constants"
"modelRT/diagram"
"github.com/RediSearch/redisearch-go/v2/redisearch"
)
// StoreComponentColumnRecommend binds token6 component config to component table column names.
func StoreComponentColumnRecommend(ctx context.Context, compTagToFullPath map[string]string, isLocalCompTagToFullPath map[string]string, componentColumnNames []string) error {
rdb := diagram.GetRedisClientInstance()
pipe := rdb.Pipeline()
pipe.SAdd(ctx, constants.RedisAllConfigSetKey, constants.ComponentConfigKey)
if len(componentColumnNames) == 0 {
_, err := pipe.Exec(ctx)
return err
}
componentColumnMembers := stringSliceToAny(componentColumnNames)
pipe.SAdd(ctx, constants.RedisAllMeasTagSetKey, componentColumnMembers...)
sug := make([]redisearch.Suggestion, 0, len(compTagToFullPath)*len(componentColumnNames)*4)
for compTag, fullPath := range compTagToFullPath {
specCompMeasKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag)
pipe.SAdd(ctx, specCompMeasKey, componentColumnMembers...)
pipe.HSet(ctx, componentColumnGroupKey(compTag), componentColumnHashFields(componentColumnNames)...)
isLocalFullPath := isLocalCompTagToFullPath[compTag]
fullConfigTerm := fmt.Sprintf("%s.%s", fullPath, constants.ComponentConfigKey)
sug = append(sug, redisearch.Suggestion{
Term: fullConfigTerm,
Score: constants.DefaultScore,
})
if isLocalFullPath != "" {
localConfigTerm := fmt.Sprintf("%s.%s", isLocalFullPath, constants.ComponentConfigKey)
sug = append(sug, redisearch.Suggestion{
Term: localConfigTerm,
Score: constants.DefaultScore,
})
}
for _, columnName := range componentColumnNames {
fullColumnTerm := fmt.Sprintf("%s.%s.%s", fullPath, constants.ComponentConfigKey, columnName)
sug = append(sug, redisearch.Suggestion{
Term: fullColumnTerm,
Score: constants.DefaultScore,
})
if isLocalFullPath == "" {
continue
}
localColumnTerm := fmt.Sprintf("%s.%s.%s", isLocalFullPath, constants.ComponentConfigKey, columnName)
sug = append(sug, redisearch.Suggestion{
Term: localColumnTerm,
Score: constants.DefaultScore,
})
}
}
if len(sug) > 0 {
ac.AddTerms(sug...)
}
_, err := pipe.Exec(ctx)
return err
}
func stringSliceToAny(values []string) []any {
members := make([]any, 0, len(values))
for _, value := range values {
members = append(members, value)
}
return members
}
func componentColumnGroupKey(compTag string) string {
return recommendGroupKey(compTag, constants.ComponentConfigKey)
}
func componentColumnHashFields(columnNames []string) []any {
return recommendHashFields(columnNames)
}

View File

@ -1,22 +0,0 @@
package model
import (
"reflect"
"testing"
)
func TestComponentColumnGroupKey(t *testing.T) {
got := componentColumnGroupKey("cable_26-demoProject110kV_TV")
want := "cable_26-demoProject110kV_TV_component"
if got != want {
t.Fatalf("expected key %q, got %q", want, got)
}
}
func TestComponentColumnHashFields(t *testing.T) {
got := componentColumnHashFields([]string{"global_uuid", "nspath"})
want := []any{"global_uuid", true, "nspath", true}
if !reflect.DeepEqual(got, want) {
t.Fatalf("expected fields %#v, got %#v", want, got)
}
}

View File

@ -22,7 +22,6 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
compTagToFullPath := make(map[string]string)
isLocalCompTagToFullPath := make(map[string]string)
var allErrs []error
zoneToGridPath := make(map[string]string)
for gridTag, zoneTags := range measSet.GridToZoneTags {
@ -63,21 +62,12 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
pipe.SAdd(ctx, key, members)
}
}
safeAddTerms := func(sug []redisearch.Suggestion) {
if len(sug) == 0 {
return
}
if err := ac.AddTerms(sug...); err != nil {
logger.Error(ctx, "add measurement group recommend suggestions failed", "error", err)
allErrs = append(allErrs, err)
}
}
safeSAdd(constants.RedisAllGridSetKey, measSet.AllGridTags)
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
})
safeAddTerms(gridSug)
ac.AddTerms(gridSug...)
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
@ -93,7 +83,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
})
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
safeAddTerms(sug)
ac.AddTerms(sug...)
}
// building the zone -> stations hierarchy
@ -111,7 +101,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
})
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
safeAddTerms(sug)
ac.AddTerms(sug...)
}
// building the station -> component nspaths hierarchy
@ -132,7 +122,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
safeAddTerms(sug)
ac.AddTerms(sug...)
}
// building the component nspath -> component tags hierarchy
@ -146,17 +136,20 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
}
for _, compTag := range compTags {
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
compTagToFullPath[compTag] = fullTerm
isLocalCompTagToFullPath[compTag] = localTerm
fullPath := fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag)
compTagToFullPath[compTag] = fullPath
fullPath = fmt.Sprintf("%s.%s", compNSPath, compTag)
isLocalCompTagToFullPath[compTag] = fullPath
// add redis fuzzy search suggestion for token1-token7 type
sug = append(sug, redisearch.Suggestion{Term: fullTerm, Score: constants.DefaultScore})
term := fullPath
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
// add redis fuzzy search suggestion for token4-token7 type
sug = append(sug, redisearch.Suggestion{Term: localTerm, Score: constants.DefaultScore})
term = fmt.Sprintf("%s.%s", compNSPath, compTag)
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
safeAddTerms(sug)
ac.AddTerms(sug...)
}
// building the component tag -> measurement tags hierarchy
@ -191,25 +184,10 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
if len(measTags) > 0 {
pipe.HSet(ctx, recommendGroupKey(compTag, "bay"), recommendHashFields(measTags)...)
}
safeAddTerms(sug)
}
// building the component nspath -> measurement tags hierarchy for token4-token7 shorthand
for compNSPath, measTags := range measSet.CompNSPathToMeasTags {
sug := make([]redisearch.Suggestion, 0, len(measTags))
for _, measTag := range measTags {
sug = append(sug, redisearch.Suggestion{
Term: fmt.Sprintf("%s.%s", compNSPath, measTag),
Score: constants.DefaultScore,
})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, compNSPath), measTags)
safeAddTerms(sug)
ac.AddTerms(sug...)
}
var allErrs []error
cmders, execErr := pipe.Exec(ctx)
if execErr != nil {
logger.Error(ctx, "pipeline execution failed", "error", execErr)
@ -233,7 +211,3 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
return compTagToFullPath, isLocalCompTagToFullPath, nil
}
func compTagSuggestionTerms(parentPath string, compNSPath string, compTag string) (string, string) {
return fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag), fmt.Sprintf("%s.%s", compNSPath, compTag)
}

View File

@ -1,21 +0,0 @@
package model
import "testing"
func TestCompTagSuggestionTermsKeepFullAndLocalPaths(t *testing.T) {
parentPath := "grid000.zone000.station000"
compNSPath := "110kV_TV-demoProject"
compTag := "cable_22-testProject1110kV_TV"
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
wantFullTerm := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV"
if fullTerm != wantFullTerm {
t.Fatalf("expected full suggestion term %q, got %q", wantFullTerm, fullTerm)
}
wantLocalTerm := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
if localTerm != wantLocalTerm {
t.Fatalf("expected local suggestion term %q, got %q", wantLocalTerm, localTerm)
}
}

View File

@ -1,4 +1,3 @@
// Package model define model struct of model runtime service
package model
import (
@ -8,12 +7,11 @@ import (
// SelectModelByType define select the data structure for parsing based on the input model type
func SelectModelByType(modelType int) BasicModelInterface {
switch modelType {
case constants.BusbarType:
if modelType == constants.BusbarType {
return &orm.BusbarSection{}
case constants.AsyncMotorType:
} else if modelType == constants.AsyncMotorType {
return &orm.AsyncMotor{}
case constants.DemoType:
} else if modelType == constants.DemoType {
return &orm.Demo{}
}
return nil

View File

@ -48,12 +48,11 @@ func CleanupRecommendRedisCache(ctx context.Context) error {
}
patterns := []string{
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
"*_nspath_measurement_tag_keys", // correspond RedisSpecCompNSPathMeasSetKey
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
}
for _, pattern := range patterns {

View File

@ -1,15 +0,0 @@
package model
import "fmt"
func recommendGroupKey(compTag string, configToken string) string {
return fmt.Sprintf("%s_%s", compTag, configToken)
}
func recommendHashFields(values []string) []any {
fields := make([]any, 0, len(values)*2)
for _, value := range values {
fields = append(fields, value, true)
}
return fields
}

File diff suppressed because it is too large Load Diff

View File

@ -1,854 +0,0 @@
package model
import (
"fmt"
"reflect"
"strings"
"testing"
"modelRT/constants"
)
func TestFuzzyRecommendOffsetUsesFullMatchedInput(t *testing.T) {
tests := []struct {
name string
searchPrefix string
searchInput string
want int
}{
{
name: "zone fuzzy prefix",
searchPrefix: "grid000",
searchInput: "z",
want: len([]rune("grid000.z")),
},
{
name: "level one fuzzy",
searchPrefix: "",
searchInput: "g",
want: len([]rune("g")),
},
{
name: "measurement fuzzy preserves config token",
searchPrefix: "grid.zone.station.nspath.comp.config",
searchInput: "m",
want: len([]rune("grid.zone.station.nspath.comp.config.m")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := fuzzyRecommendOffset(tt.searchPrefix, tt.searchInput)
if got != tt.want {
t.Fatalf("expected offset %d, got %d", tt.want, got)
}
})
}
}
func TestFuzzyRecommendMemberSetKeyUsesLocalNSPathSet(t *testing.T) {
setKey, ok := fuzzyRecommendMemberSetKey(constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, "")
if !ok {
t.Fatalf("expected local nspath fuzzy member check to use a redis set")
}
if setKey != constants.RedisAllCompNSPathSetKey {
t.Fatalf("expected set key %q, got %q", constants.RedisAllCompNSPathSetKey, setKey)
}
}
func TestNormalizeRecommendResultsKeepsFuzzyExactContinuation(t *testing.T) {
tests := []struct {
name string
input string
recommendType constants.RecommendHierarchyType
offset int
}{
{
name: "local nspath typo completes current level",
input: "110kV_TV-demoProjectx",
recommendType: constants.CompNSPathRecommendHierarchyType,
offset: len([]rune("110kV_TV-demoProject")),
},
{
name: "grid typo completes current level",
input: "grid000x",
recommendType: constants.GridRecommendHierarchyType,
offset: len([]rune("grid000")),
},
{
name: "zone typo completes current level",
input: "grid000.zone000x",
recommendType: constants.ZoneRecommendHierarchyType,
offset: len([]rune("grid000.zone000")),
},
{
name: "station typo completes current level",
input: "grid000.zone000.station000x",
recommendType: constants.StationRecommendHierarchyType,
offset: len([]rune("grid000.zone000.station000")),
},
{
name: "full nspath typo completes current level",
input: "grid000.zone000.station000.110kV_TV-demoProjectx",
recommendType: constants.CompNSPathRecommendHierarchyType,
offset: len([]rune("grid000.zone000.station000.110kV_TV-demoProject")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := map[string]SearchResult{
tt.recommendType.String(): {
RecommendType: tt.recommendType,
QueryDatas: []string{"."},
IsFuzzy: true,
Offset: tt.offset,
},
}
got := normalizeRecommendResults(tt.input, results)
result := got[tt.recommendType.String()]
if result.Offset != tt.offset {
t.Fatalf("expected offset %d, got %d", tt.offset, result.Offset)
}
if !reflect.DeepEqual(result.QueryDatas, []string{"."}) {
t.Fatalf("expected fuzzy exact continuation '.', got %#v", result.QueryDatas)
}
})
}
}
func TestNormalizeRecommendResultsUsesInputLengthForExactCompletion(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "full token1 to token7 structure",
input: "grid000.zone000.station000.220kV_学府路1-testProject1.compTag.config.IA_rms_CTA-testProject1",
},
{
name: "local token4 to token7 structure",
input: "220kV_学府路1-testProject1.IA_rms_CTA-testProject1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: []string{""},
IsFuzzy: false,
},
}
got := normalizeRecommendResults(tt.input, results)
result := got[constants.MeasTagRecommendHierarchyType.String()]
wantOffset := len([]rune(tt.input))
if result.Offset != wantOffset {
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
}
if len(result.QueryDatas) != 0 {
t.Fatalf("expected exact completion to return no suffix, got %v", result.QueryDatas)
}
})
}
}
func TestNormalizeRecommendResultsUsesInputLengthForLevelContinuation(t *testing.T) {
tests := []struct {
name string
input string
recommendType constants.RecommendHierarchyType
}{
{
name: "token1 grid can continue",
input: "grid000",
recommendType: constants.GridRecommendHierarchyType,
},
{
name: "token1 token2 zone can continue",
input: "grid000.zone000",
recommendType: constants.ZoneRecommendHierarchyType,
},
{
name: "token1 token2 token3 station can continue",
input: "grid000.zone000.station000",
recommendType: constants.StationRecommendHierarchyType,
},
{
name: "token1 token2 token3 token4 nspath can continue",
input: "grid000.zone000.station000.110kV_TV-demoProject",
recommendType: constants.CompNSPathRecommendHierarchyType,
},
{
name: "token1 token2 token3 token4 token5 compTag can continue",
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV",
recommendType: constants.CompTagRecommendHierarchyType,
},
{
name: "token1 through token6 config can continue",
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV.base_extend",
recommendType: constants.ConfigRecommendHierarchyType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results := map[string]SearchResult{
tt.recommendType.String(): {
RecommendType: tt.recommendType,
QueryDatas: []string{"."},
IsFuzzy: false,
},
}
got := normalizeRecommendResults(tt.input, results)
result := got[tt.recommendType.String()]
wantOffset := len([]rune(tt.input))
if result.Offset != wantOffset {
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
}
if len(result.QueryDatas) != 1 || result.QueryDatas[0] != "." {
t.Fatalf("expected level continuation '.', got %v", result.QueryDatas)
}
})
}
}
func TestNormalizeRecommendResultsKeepsFallbackOffsetZero(t *testing.T) {
input := "x"
results := map[string]SearchResult{
constants.GridRecommendHierarchyType.String(): {
RecommendType: constants.GridRecommendHierarchyType,
QueryDatas: []string{"grid000", "grid001"},
IsFuzzy: true,
IsFallback: true,
Offset: 0,
},
constants.CompNSPathRecommendHierarchyType.String(): {
RecommendType: constants.CompNSPathRecommendHierarchyType,
QueryDatas: []string{"nspath000", "nspath001"},
IsFuzzy: true,
IsFallback: true,
Offset: 0,
},
}
got := normalizeRecommendResults(input, results)
for key, result := range got {
if result.Offset != 0 {
t.Fatalf("expected fallback offset 0 for %s, got %d", key, result.Offset)
}
if len(result.QueryDatas) != 2 {
t.Fatalf("expected fallback recommends to remain intact for %s, got %v", key, result.QueryDatas)
}
}
}
func TestNormalizeRecommendResultsKeepsParentPrefixOffsetForSpecificFallback(t *testing.T) {
nsPath := "220kV_学府路1-testProject1"
input := nsPath + ".x"
offset := recommendPrefixOffset([]string{nsPath})
results := map[string]SearchResult{
constants.CompTagRecommendHierarchyType.String(): {
RecommendType: constants.CompTagRecommendHierarchyType,
QueryDatas: []string{
nsPath + ".CTA-testProject1220kV_学府路1",
nsPath + ".CB-testProject1220kV_学府路1",
},
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: []string{
nsPath + ".IA_rms_CTA-testProject1",
nsPath + ".IB_rms_CTA-testProject1",
},
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
}
got := normalizeRecommendResults(input, results)
if offset != 24 {
t.Fatalf("expected sample parent prefix offset 24, got %d", offset)
}
for key, result := range got {
if result.Offset != 24 {
t.Fatalf("expected specific fallback offset 24 for %s, got %d", key, result.Offset)
}
for _, recommend := range result.QueryDatas {
if strings.HasPrefix(recommend, nsPath+".") {
t.Fatalf("expected trimmed fallback recommend for %s, got %s", key, recommend)
}
}
}
}
func TestNormalizeRecommendResultsTrimsConfigFuzzySuffixes(t *testing.T) {
input := "110kV_TV-testProject1.cable_22-testProject1110kV_TV.base_extend.c"
offset := len([]rune(input))
results := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: []string{
input + "apacity",
input + "ategory",
input + "urrent",
input + "ode",
},
IsFuzzy: true,
Offset: offset,
},
}
got := normalizeRecommendResults(input, results)
result := got[constants.MeasTagRecommendHierarchyType.String()]
want := []string{"apacity", "ategory", "urrent", "ode"}
if result.Offset != offset {
t.Fatalf("expected offset %d, got %d", offset, result.Offset)
}
if !reflect.DeepEqual(result.QueryDatas, want) {
t.Fatalf("expected trimmed recommends %#v, got %#v", want, result.QueryDatas)
}
}
func TestNormalizeRecommendResultsTrimsToken4FallbackCandidates(t *testing.T) {
nsPath := "110kV_TV-testProject1"
input := nsPath + ".x"
offset := recommendPrefixOffset([]string{nsPath})
results := map[string]SearchResult{
constants.CompTagRecommendHierarchyType.String(): {
RecommendType: constants.CompTagRecommendHierarchyType,
QueryDatas: []string{
nsPath + ".cable_22-testProject1110kV_TV",
nsPath + ".cable_23-testProject1110kV_TV",
},
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: []string{
nsPath + ".IA_rms_CTA-testProject1",
nsPath + ".IB_rms_CTA-testProject1",
},
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
}
got := normalizeRecommendResults(input, results)
for key, result := range got {
if result.Offset != offset {
t.Fatalf("expected offset %d for %s, got %d", offset, key, result.Offset)
}
for _, recommend := range result.QueryDatas {
if strings.HasPrefix(recommend, nsPath+".") {
t.Fatalf("expected trimmed token4 fallback recommend for %s, got %s", key, recommend)
}
}
}
if got[constants.CompTagRecommendHierarchyType.String()].QueryDatas[0] != "cable_22-testProject1110kV_TV" {
t.Fatalf("expected token5 suffixes, got %v", got[constants.CompTagRecommendHierarchyType.String()].QueryDatas)
}
if got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas[0] != "IA_rms_CTA-testProject1" {
t.Fatalf("expected token7 suffixes, got %v", got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas)
}
}
func TestNormalizeRecommendResultsKeepsFullAndLocalConfigMeasurementSuffixesConsistent(t *testing.T) {
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
members := []string{"capacity", "category", "current"}
fullResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
IsFuzzy: false,
},
}
localResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
IsFuzzy: false,
},
}
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
localGot := normalizeRecommendResults(localPrefix+".", localResults)
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
if !reflect.DeepEqual(fullRecommends, localRecommends) {
t.Fatalf("expected full/local config measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
}
}
func TestNormalizeRecommendResultsKeepsFullAndLocalBayMeasurementSuffixesConsistent(t *testing.T) {
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
members := []string{"IA_rms", "IB_rms", "IC_rms"}
fullResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
IsFuzzy: false,
},
}
localResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
IsFuzzy: false,
},
}
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
localGot := normalizeRecommendResults(localPrefix+".", localResults)
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
if !reflect.DeepEqual(fullRecommends, localRecommends) {
t.Fatalf("expected full/local bay measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
}
}
func TestNormalizeRecommendResultsKeepsConfigMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
prefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
members := []string{"bay", "base_extend", "model", "component"}
groupResults := combineQueryResultByInput(constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(prefix+".", "."), members)
offset := recommendPrefixOffset(strings.Split(prefix, "."))
emptyInputResults := map[string]SearchResult{
constants.ConfigRecommendHierarchyType.String(): {
RecommendType: constants.ConfigRecommendHierarchyType,
QueryDatas: append([]string{}, groupResults...),
IsFuzzy: false,
},
}
mismatchFallbackResults := map[string]SearchResult{
constants.ConfigRecommendHierarchyType.String(): {
RecommendType: constants.ConfigRecommendHierarchyType,
QueryDatas: append([]string{}, groupResults...),
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
}
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
emptyResult := emptyGot[constants.ConfigRecommendHierarchyType.String()]
mismatchResult := mismatchGot[constants.ConfigRecommendHierarchyType.String()]
if mismatchResult.Offset != emptyResult.Offset {
t.Fatalf("expected config mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
}
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
t.Fatalf("expected config mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
}
}
func TestNormalizeRecommendResultsKeepsMeasurementMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
prefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
members := []string{"IA_rms_CTA-testProject1", "IB_rms_CTA-testProject1", "IC_rms_CTA-testProject1"}
groupResults := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(prefix+".", "."), members)
offset := recommendPrefixOffset(strings.Split(prefix, "."))
emptyInputResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: append([]string{}, groupResults...),
IsFuzzy: false,
},
}
mismatchFallbackResults := map[string]SearchResult{
constants.MeasTagRecommendHierarchyType.String(): {
RecommendType: constants.MeasTagRecommendHierarchyType,
QueryDatas: append([]string{}, groupResults...),
IsFuzzy: true,
IsFallback: true,
Offset: offset,
},
}
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
emptyResult := emptyGot[constants.MeasTagRecommendHierarchyType.String()]
mismatchResult := mismatchGot[constants.MeasTagRecommendHierarchyType.String()]
if mismatchResult.Offset != emptyResult.Offset {
t.Fatalf("expected measurement mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
}
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
t.Fatalf("expected measurement mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
}
}
func TestRecommendGroupTokens(t *testing.T) {
tests := []struct {
name string
inputSlice []string
wantCompTag string
wantConfig string
wantOK bool
}{
{
name: "full token1 to token7 input",
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "base_extend", ""},
wantCompTag: "comp_tag",
wantConfig: "base_extend",
wantOK: true,
},
{
name: "local token4 to token7 input",
inputSlice: []string{"nspath", "comp_tag", "component", ""},
wantCompTag: "comp_tag",
wantConfig: "component",
wantOK: true,
},
{
name: "missing config token",
inputSlice: []string{"nspath", ""},
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCompTag, gotConfig, gotOK := recommendGroupTokens(tt.inputSlice)
if gotOK != tt.wantOK {
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
}
if gotCompTag != tt.wantCompTag || gotConfig != tt.wantConfig {
t.Fatalf("expected compTag/config %q/%q, got %q/%q", tt.wantCompTag, tt.wantConfig, gotCompTag, gotConfig)
}
})
}
}
func TestConfigRecommendCompTag(t *testing.T) {
tests := []struct {
name string
inputSlice []string
want string
wantOK bool
}{
{
name: "full token1 to token6 input",
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
want: "comp_tag",
wantOK: true,
},
{
name: "local token4 to token6 input",
inputSlice: []string{"nspath", "comp_tag", "b"},
want: "comp_tag",
wantOK: true,
},
{
name: "missing comp tag",
inputSlice: []string{"nspath"},
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotOK := configRecommendCompTag(tt.inputSlice)
if gotOK != tt.wantOK {
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
}
if got != tt.want {
t.Fatalf("expected compTag %q, got %q", tt.want, got)
}
})
}
}
func TestFilterMembersByTrimmedPrefix(t *testing.T) {
tests := []struct {
name string
members []string
searchInput string
wantMembers []string
wantMatchInput string
}{
{
name: "config typo falls back to previous rune",
members: []string{"bay", "base_extend", "model", "rated", "stable", "component"},
searchInput: "bx",
wantMembers: []string{"bay", "base_extend"},
wantMatchInput: "b",
},
{
name: "measurement typo falls back to previous rune",
members: []string{"I_A_rms", "I_B_rms", "U_A_rms"},
searchInput: "Ix",
wantMembers: []string{"I_A_rms", "I_B_rms"},
wantMatchInput: "I",
},
{
name: "no fallback to empty prefix",
members: []string{"bay", "base_extend"},
searchInput: "x",
wantMembers: []string{},
wantMatchInput: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotMembers, gotMatchInput := filterMembersByTrimmedPrefix(tt.members, tt.searchInput)
if !reflect.DeepEqual(gotMembers, tt.wantMembers) {
t.Fatalf("expected members %#v, got %#v", tt.wantMembers, gotMembers)
}
if gotMatchInput != tt.wantMatchInput {
t.Fatalf("expected matched input %q, got %q", tt.wantMatchInput, gotMatchInput)
}
})
}
}
func TestDotAndTypoInputsShareTrimmedPrefixFallback(t *testing.T) {
tests := []struct {
name string
dotInput string
typoInput string
members []string
wantSuffix []string
}{
{
name: "token1 grid",
dotInput: "g.",
typoInput: "gx",
members: []string{"grid000", "grid001", "zone000"},
wantSuffix: []string{"grid000", "grid001"},
},
{
name: "token2 zone",
dotInput: "z.",
typoInput: "zx",
members: []string{"zone000", "zone001", "station000"},
wantSuffix: []string{"zone000", "zone001"},
},
{
name: "token3 station",
dotInput: "s.",
typoInput: "sx",
members: []string{"station000", "station001", "zone000"},
wantSuffix: []string{"station000", "station001"},
},
{
name: "token4 nspath",
dotInput: "1.",
typoInput: "1x",
members: []string{"110kV_TV-demoProject", "110kV_TV-testProject1", "220kV_TV-demoProject"},
wantSuffix: []string{"110kV_TV-demoProject", "110kV_TV-testProject1"},
},
{
name: "token5 component tag",
dotInput: "c.",
typoInput: "cx",
members: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV", "bay"},
wantSuffix: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV"},
},
{
name: "token6 config",
dotInput: "b.",
typoInput: "bx",
members: []string{"bay", "base_extend", "component"},
wantSuffix: []string{"bay", "base_extend"},
},
{
name: "token7 measurement",
dotInput: "I.",
typoInput: "Ix",
members: []string{"IA_rms", "IB_rms", "UA_rms"},
wantSuffix: []string{"IA_rms", "IB_rms"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dotSearchInput := strings.TrimSuffix(tt.dotInput, ".")
typoSearchInput := trimLastRune(tt.typoInput)
dotMembers, dotMatchedInput := filterMembersByTrimmedPrefix(tt.members, dotSearchInput)
typoMembers, typoMatchedInput := filterMembersByTrimmedPrefix(tt.members, typoSearchInput)
if dotMatchedInput != typoMatchedInput {
t.Fatalf("expected dot and typo matched input to be equal, dot=%q typo=%q", dotMatchedInput, typoMatchedInput)
}
if !reflect.DeepEqual(dotMembers, typoMembers) {
t.Fatalf("expected dot and typo members to be equal, dot=%#v typo=%#v", dotMembers, typoMembers)
}
if !reflect.DeepEqual(dotMembers, tt.wantSuffix) {
t.Fatalf("expected members %#v, got %#v", tt.wantSuffix, dotMembers)
}
})
}
}
func TestExactMatchChecksForInput(t *testing.T) {
tests := []struct {
name string
inputSlice []string
want []exactMatchCheck
}{
{
name: "token1 can be grid or local nspath",
inputSlice: []string{"g"},
want: []exactMatchCheck{
{setKey: constants.RedisAllGridSetKey, member: "g"},
{setKey: constants.RedisAllCompNSPathSetKey, member: "g"},
},
},
{
name: "token1 token2 can be zone comp tag or nspath meas",
inputSlice: []string{"grid000", "z"},
want: []exactMatchCheck{
{setKey: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"), member: "z"},
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"), member: "z"},
{setKey: constants.RedisAllCompTagSetKey, member: "z"},
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "grid000"), member: "z"},
},
},
{
name: "token1 token2 token3 can be station or local config",
inputSlice: []string{"grid000", "zone000", "s"},
want: []exactMatchCheck{
{setKey: fmt.Sprintf(constants.RedisSpecZoneStationSetKey, "zone000"), member: "s"},
{setKey: constants.RedisAllConfigSetKey, member: "s"},
},
},
{
name: "token1 token2 token3 token4 can be nspath or local meas",
inputSlice: []string{"grid000", "zone000", "station000", "1"},
want: []exactMatchCheck{
{setKey: fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, "station000"), member: "1"},
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "zone000"), member: "1"},
},
},
{
name: "token1 through token5 component tag",
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "c"},
want: []exactMatchCheck{
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "nspath"), member: "c"},
{setKey: constants.RedisAllCompTagSetKey, member: "c"},
},
},
{
name: "token1 through token6 config",
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
want: []exactMatchCheck{
{setKey: constants.RedisAllConfigSetKey, member: "b"},
},
},
{
name: "token1 through token7 measurement",
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "bay", "I"},
want: []exactMatchCheck{
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), member: "I"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := exactMatchChecksForInput(tt.inputSlice)
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("expected checks %#v, got %#v", tt.want, got)
}
})
}
}
func TestFallbackSpecificSetKey(t *testing.T) {
tests := []struct {
name string
hierarchy constants.RecommendHierarchyType
inputSlice []string
want string
wantOK bool
}{
{
name: "zone fallback uses grid specific set",
hierarchy: constants.ZoneRecommendHierarchyType,
inputSlice: []string{"grid000", "z"},
want: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"),
wantOK: true,
},
{
name: "component tag fallback uses previous nspath token",
hierarchy: constants.CompTagRecommendHierarchyType,
inputSlice: []string{"grid000", "I"},
want: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"),
wantOK: true,
},
{
name: "measurement fallback skips config token and uses component tag",
hierarchy: constants.MeasTagRecommendHierarchyType,
inputSlice: []string{"nspath", "comp_tag", "config", "m"},
want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"),
wantOK: true,
},
{
name: "config fallback has no parent specific set",
hierarchy: constants.ConfigRecommendHierarchyType,
inputSlice: []string{"nspath", "comp_tag", ""},
wantOK: false,
},
{
name: "missing previous token",
hierarchy: constants.ZoneRecommendHierarchyType,
inputSlice: []string{"z"},
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := fallbackSpecificSetKey(tt.hierarchy, tt.inputSlice)
if ok != tt.wantOK {
t.Fatalf("expected ok %t, got %t", tt.wantOK, ok)
}
if got != tt.want {
t.Fatalf("expected key %q, got %q", tt.want, got)
}
})
}
}
func TestShouldFallbackToInitialRecommend(t *testing.T) {
tests := []struct {
input string
want bool
}{
{input: "", want: false},
{input: ".", want: true},
{input: ".x", want: true},
{input: "..x", want: true},
{input: "...x", want: true},
{input: "grid000", want: false},
{input: "grid000.", want: false},
{input: "grid000.zone000", want: false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := shouldFallbackToInitialRecommend(tt.input)
if got != tt.want {
t.Fatalf("expected %t, got %t", tt.want, got)
}
})
}
}

View File

@ -26,6 +26,7 @@ type WSResponse struct {
type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"`
RecommendType string `json:"recommended_type" example:"grid_tag"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
}

View File

@ -16,5 +16,4 @@ type MeasurementSet struct {
StationToCompNSPaths map[string][]string // Key: StationTag, Value: NSPaths
CompNSPathToCompTags map[string][]string // Key: NSPaths, Value: CompTags
CompTagToMeasTags map[string][]string // Key: CompTag, Value: MeasTags
CompNSPathToMeasTags map[string][]string // Key: NSPaths, Value: MeasTags
}

View File

@ -12,3 +12,18 @@ var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
JOIN recursive_tree rt ON t.uuid_from = rt.uuid_to
)
SELECT * FROM recursive_tree;`
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
// supplied start component. It tracks the visited node path inside PostgreSQL
// so cycles in topologic data cannot recurse forever.
var RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
FROM "topologic"
WHERE uuid_from = ?
UNION ALL
SELECT t.uuid_from, t.uuid_to, t.flag, rt.path || t.uuid_to
FROM "topologic" t
JOIN recursive_tree rt ON t.uuid_from = rt.uuid_to
WHERE NOT t.uuid_to = ANY(rt.path)
)
SELECT uuid_from, uuid_to, flag FROM recursive_tree;`

View File

@ -98,10 +98,10 @@ func NewTopologyAnalysisHandler() *TopologyAnalysisHandler {
}
}
// Execute processes a topology analysis task.
// Execute processes a point-to-point topology reachability task.
// Params (all sourced from the MQ message, no DB lookup needed):
// - start_component_uuid (string, required): BFS origin
// - end_component_uuid (string, required): reachability target
// - start_component_uuid (string, required): directed traversal origin
// - end_component_uuid (string, required): directed reachability target
// - check_in_service (bool, optional, default true): skip out-of-service components
func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID, params map[string]any, db *gorm.DB) error {
logger.Info(ctx, "topology analysis started", "task_id", taskID)
@ -123,7 +123,8 @@ func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID,
logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 20, "error", err)
}
// Phase 2: query topology edges from startComponentUUID, build adjacency list
// Phase 2: query only edges reachable from startComponentUUID, then build
// the adjacency list used for point-to-point directed reachability.
topoEdges, err := database.QueryTopologicByStartUUID(ctx, db, startComponentUUID)
if err != nil {
return fmt.Errorf("query topology from start node: %w", err)
@ -159,7 +160,7 @@ func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID,
// check the start node itself before BFS
if !inServiceMap[startComponentUUID] {
return persistTopologyResult(ctx, db, taskID, startComponentUUID, endComponentUUID,
checkInService, false, nil, &startComponentUUID)
checkInService, false, nil, &startComponentUUID, 0)
}
}
@ -167,7 +168,8 @@ func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID,
logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 60, "error", err)
}
// Phase 4: BFS reachability check
// Phase 4: point-to-point BFS reachability check. Multiple parents and
// multiple paths to a node are valid; visited only prevents cycles/rework.
visited := make(map[uuid.UUID]struct{})
parent := make(map[uuid.UUID]uuid.UUID) // for path reconstruction
queue := []uuid.UUID{startComponentUUID}
@ -214,7 +216,7 @@ func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID,
}
return persistTopologyResult(ctx, db, taskID, startComponentUUID, endComponentUUID,
checkInService, isReachable, path, blockedBy)
checkInService, isReachable, path, blockedBy, len(visited))
}
// parseTopologyAnalysisParams extracts and validates the three required fields.
@ -270,6 +272,7 @@ func persistTopologyResult(
ctx context.Context, db *gorm.DB, taskID uuid.UUID,
startID, endID uuid.UUID, checkInService, isReachable bool,
path []uuid.UUID, blockedBy *uuid.UUID,
visitedCount int,
) error {
pathStrs := make([]string, 0, len(path))
for _, id := range path {
@ -281,11 +284,22 @@ func persistTopologyResult(
"end_component_uuid": endID.String(),
"check_in_service": checkInService,
"is_reachable": isReachable,
"analysis_type": "POINT_TO_POINT_REACHABILITY",
"path": pathStrs,
"path_node_count": len(pathStrs),
"visited_count": visitedCount,
"computed_at": time.Now().Unix(),
}
if isReachable {
result["hop_count"] = len(pathStrs) - 1
}
if blockedBy != nil {
result["blocked_by"] = blockedBy.String()
result["reason"] = "OUT_OF_SERVICE_COMPONENT"
} else if isReachable {
result["reason"] = "REACHABLE"
} else {
result["reason"] = "NO_DIRECTED_PATH"
}
if err := database.CreateAsyncTaskResult(ctx, db, taskID, result); err != nil {

View File

@ -46,12 +46,10 @@ func GetLongestCommonPrefixLength(query string, result string) int {
return 0
}
queryRunes := []rune(query)
resultRunes := []rune(result)
minLen := min(len(queryRunes), len(resultRunes))
minLen := min(len(query), len(result))
for i := range minLen {
if queryRunes[i] != resultRunes[i] {
if query[i] != result[i] {
return i
}
}

View File

@ -6,8 +6,8 @@ import (
"time"
)
// GenNanoTSStr define func to generate nanosecond timestamp string by current time
func GenNanoTSStr() string {
// GenNanoTsStr define func to generate nanosecond timestamp string by current time
func GenNanoTsStr() string {
now := time.Now()
nanoseconds := now.UnixNano()
timestampStr := strconv.FormatInt(nanoseconds, 10)