fix(backend): implement singleton pattern for anonymizer replacer

- Introduced a process-level singleton for the anonymizer replacer to enhance performance and ensure thread safety.
- The replacer is built once using patterns loaded at startup, allowing it to be shared across all flow executor instances.
- Updated the flow tools executor to utilize the shared replacer, simplifying the creation process and improving efficiency.
This commit is contained in:
Dmitry Ng
2026-06-25 12:28:49 +03:00
parent 9caa6a8775
commit e909f40b2e
+26 -11
View File
@@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"time"
"pentagi/pkg/config"
@@ -348,6 +349,16 @@ type FlowToolsExecutor interface {
GetReporterExecutor(cfg ReporterExecutorConfig) (ContextToolsExecutor, error)
}
// sharedReplacer is a process-level singleton for the anonymizer replacer.
// The replacer is built from immutable sources (embedded patterns + startup
// config), so it is safe to share across all flow executor instances.
// ReplaceString is a pure read operation and is goroutine-safe.
var (
sharedReplacer anonymizer.Replacer
sharedReplacerOnce sync.Once
sharedReplacerErr error
)
func NewFlowToolsExecutor(
db database.Querier,
cfg *config.Config,
@@ -355,24 +366,28 @@ func NewFlowToolsExecutor(
functions *Functions,
userID, flowID int64,
) (FlowToolsExecutor, error) {
allPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
if err != nil {
return nil, fmt.Errorf("failed to load all patterns: %v", err)
}
// Build the replacer exactly once for the lifetime of the process.
// cfg.GetSecretPatterns() reads environment variables that are fixed at
// startup, so the first caller's config is representative for all callers.
sharedReplacerOnce.Do(func() {
allPatterns, err := patterns.LoadPatterns(patterns.PatternListTypeAll)
if err != nil {
sharedReplacerErr = fmt.Errorf("failed to load all patterns: %v", err)
return
}
allPatterns.Patterns = append(allPatterns.Patterns, cfg.GetSecretPatterns()...)
sharedReplacer, sharedReplacerErr = anonymizer.NewReplacer(allPatterns.Regexes(), allPatterns.Names())
})
// combine with config secret patterns
allPatterns.Patterns = append(allPatterns.Patterns, cfg.GetSecretPatterns()...)
replacer, err := anonymizer.NewReplacer(allPatterns.Regexes(), allPatterns.Names())
if err != nil {
return nil, fmt.Errorf("failed to create replacer: %v", err)
if sharedReplacerErr != nil {
return nil, fmt.Errorf("failed to create replacer: %v", sharedReplacerErr)
}
return &flowToolsExecutor{
db: db,
docker: docker,
functions: functions,
replacer: replacer,
replacer: sharedReplacer,
cfg: cfg,
evidence: newEvidenceReceiptRecorder(cfg.DataDir, flowID, cfg.EvidenceReceiptsEnabled),
flowID: flowID,