Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ func dumpMultiLevelCacheToFile(cache *MultiLevelCache, filename string) error {
// DumpForestToFile dumps the forest in concise graph format to a file
func DumpForestToFile(m *InMemoryMatcher, filename string) error {
// We'll produce two files per requested filename:
// - <filename>.graph : concise graph listing edges between node keys (dimension|value+match)
// - <filename>.mapping : mapping of node_key -> comma-separated rule IDs (for lookup)
// - <filename>.mermaid : concise graph listing edges between node keys (dimension#value+match)

m.mu.RLock()
// Snapshot the forestIndexes keys to avoid holding matcher lock while writing files
Expand All @@ -134,24 +133,27 @@ func DumpForestToFile(m *InMemoryMatcher, filename string) error {
forest := forestIndex.RuleForest

// Tenant header
graphLines = append(graphLines, fmt.Sprintf("# Tenant: %s", tenantKey))
mappingLines = append(mappingLines, fmt.Sprintf("# Tenant: %s", tenantKey))
graphs, relationship := make(map[string]any), ""

// Snapshot NodeRelationships under forest lock
forest.mu.RLock()
for current, trans := range forest.NodeRelationships {
b := strings.Builder{}
for rid, next := range trans {
relationship = fmt.Sprintf("%s %s", current, next)
if _, ok := graphs[relationship]; !ok {
graphs[relationship] = nil
graphLines = append(graphLines, relationship)
if next != "" {
// Not the last node
relationship = fmt.Sprintf(" %s --> %s", current, next)
if _, ok := graphs[relationship]; !ok {
graphs[relationship] = nil
graphLines = append(graphLines, relationship)
}
}
if b.Len() > 0 {
b.WriteString(",")
}
b.WriteString(rid)
Comment thread
massiveio marked this conversation as resolved.
b.WriteString(",")
}
mappingLines = append(mappingLines, fmt.Sprintf("%s %s", current, b.String()))
mappingLines = append(mappingLines, fmt.Sprintf(" %s[%s<%s>]", current, current, b.String()))
}
forest.mu.RUnlock()

Expand All @@ -161,16 +163,13 @@ func DumpForestToFile(m *InMemoryMatcher, filename string) error {
}

// Write graph file
graphFile := filename + ".graph"
if err := os.WriteFile(graphFile, []byte(strings.Join(graphLines, "\n")), 0644); err != nil {
graphFile := filename + ".mermaid"
graph := append([]string{}, "flowchart TD")
graph = append(graph, mappingLines...)
graph = append(graph, graphLines...)
if err := os.WriteFile(graphFile, []byte(strings.Join(graph, "\n")), 0644); err != nil {
return fmt.Errorf("failed to write forest graph file: %w", err)
}

// Write mapping file
mappingFile := filename + ".mapping"
if err := os.WriteFile(mappingFile, []byte(strings.Join(mappingLines, "\n")), 0644); err != nil {
return fmt.Errorf("failed to write forest mapping file: %w", err)
}

return nil
}
54 changes: 27 additions & 27 deletions forest.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,16 @@ func CreateForestIndexCompat() *RuleForest {

// generateNodeName generates a unique node name in the pattern 'dimension|value+match_type'
func generateNodeName(dimensionName, value string, matchType MatchType) string {
var matchTypeStr string
switch matchType {
case MatchTypeEqual:
matchTypeStr = ""
case MatchTypeAny:
matchTypeStr = "*"
value = "*"
case MatchTypePrefix:
matchTypeStr = "*"
value = value + "*"
case MatchTypeSuffix:
matchTypeStr = "*"
value = "*" + value
}
Comment thread
massiveio marked this conversation as resolved.
return fmt.Sprintf("%s|%s%s", dimensionName, value, matchTypeStr)
return fmt.Sprintf("%s#%s", dimensionName, value)
}

// cleanupNodeRelationshipsForRule removes relationships for a specific rule from a specific node's relationships.
Expand Down Expand Up @@ -223,55 +219,54 @@ func (rf *RuleForest) AddRule(rule *Rule) (*Rule, error) {
ruleNodes = append(ruleNodes, rootNode)

// Track parent node for relationship building
var parentNodeName string
var parentNodeName, childNodeName string

// Traverse/create path for remaining dimensions
current := rootNode
parent := rootNode
parentDim := firstDim
parentNodeName = generateNodeName(parent.DimensionName, parent.Value, parentDim.MatchType)
for i := 1; i < len(sorted); i++ {
dim := completeRule.GetDimensionValue(sorted[i])
currentDim := completeRule.GetDimensionValue(sorted[i])

// Use the original match type from the rule definition - do NOT change it
matchType := dim.MatchType
currentMatchType := currentDim.MatchType

// Get or create the match branch for the CURRENT dimension's match type
branch, exists := current.Branches[matchType]
branch, exists := parent.Branches[currentMatchType]
if !exists {
branch = &MatchBranch{
MatchType: matchType,
MatchType: currentMatchType,
Rules: []*Rule{},
Children: make(map[string]*SharedNode),
}
current.Branches[matchType] = branch
parent.Branches[currentMatchType] = branch
}

// Get or create child node for this dimension value within the match branch
child, exists := branch.Children[dim.Value]
// Get or create current node for this dimension value within the match branch
current, exists := branch.Children[currentDim.Value]
if !exists {
child = CreateSharedNode(i, dim.DimensionName, dim.Value)
branch.Children[dim.Value] = child
current = CreateSharedNode(i, currentDim.DimensionName, currentDim.Value)
branch.Children[currentDim.Value] = current

// MAINTAIN RELATIONSHIPS: Track parent-child relationship for efficient dumping
parentNodeName = generateNodeName(current.DimensionName, current.Value, dim.MatchType)
childNodeName := generateNodeName(child.DimensionName, child.Value, matchType)

// MAINTAIN NODE RELATIONSHIPS: Track rule transitions for efficient dumping
childNodeName = generateNodeName(current.DimensionName, current.Value, currentMatchType)
if rf.NodeRelationships[parentNodeName] == nil {
rf.NodeRelationships[parentNodeName] = make(map[string]string)
}
rf.NodeRelationships[parentNodeName][completeRule.ID] = childNodeName
} else {
// Even if child exists, still record the rule transition
parentNodeName = generateNodeName(current.DimensionName, current.Value, dim.MatchType)
childNodeName := generateNodeName(child.DimensionName, child.Value, matchType)

childNodeName = generateNodeName(current.DimensionName, current.Value, currentMatchType)
if rf.NodeRelationships[parentNodeName] == nil {
rf.NodeRelationships[parentNodeName] = make(map[string]string)
}
rf.NodeRelationships[parentNodeName][completeRule.ID] = childNodeName
}

ruleNodes = append(ruleNodes, child)
current = child
ruleNodes = append(ruleNodes, current)
parent = current
parentDim = currentDim
parentNodeName = generateNodeName(parent.DimensionName, parent.Value, parentDim.MatchType)
}

// Add rule to the final node (the node for the last dimension the rule specifies)
Expand All @@ -283,7 +278,12 @@ func (rf *RuleForest) AddRule(rule *Rule) (*Rule, error) {
finalMatchType = lastDim.MatchType
}
}
current.AddRule(completeRule, finalMatchType)
// Last dimension node
if _, ok := rf.NodeRelationships[childNodeName]; !ok {
rf.NodeRelationships[childNodeName] = make(map[string]string)
}
rf.NodeRelationships[childNodeName][completeRule.ID] = ""
parent.AddRule(completeRule, finalMatchType)

// Index the rule for quick removal
rf.RuleIndex[completeRule.ID] = ruleNodes
Expand Down
4 changes: 2 additions & 2 deletions matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"time"
)

const snapshotFileName = "snapshot" // .graph, .cache, .mapping
const snapshotFileName = "snapshot" // .mermaid, .cache

// InMemoryMatcher implements the core matching logic using forest indexes
type InMemoryMatcher struct {
Expand Down Expand Up @@ -124,7 +124,7 @@ func (m *InMemoryMatcher) startSnapshotMonitor() {
} else {
slog.Info("Snapshot dump complete, check snapshot.*.")
}
} else if st, err := os.Stat(snapshotFileName + ".graph"); err != nil || st.Size() <= 0 {
} else if st, err := os.Stat(snapshotFileName + ".mermaid"); err != nil || st.Size() <= 0 {
// First time snapshot generation
slog.Info("No snapshot file found, triggering initial snapshot")
atomic.CompareAndSwapInt64(&m.snapshotChanged, 0, 1)
Expand Down