From e909f40b2eafef3528eb75d40285c1b515c33ea1 Mon Sep 17 00:00:00 2001 From: Dmitry Ng <19asdek91@gmail.com> Date: Thu, 25 Jun 2026 12:28:49 +0300 Subject: [PATCH] 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. --- backend/pkg/tools/tools.go | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/backend/pkg/tools/tools.go b/backend/pkg/tools/tools.go index 17a77834..395034fd 100644 --- a/backend/pkg/tools/tools.go +++ b/backend/pkg/tools/tools.go @@ -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,