Skip to content

feat(lib): reuse metadata cache across thread-safe scans - #7608

Merged
Mzack9999 merged 3 commits into
devfrom
dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans
Jul 26, 2026
Merged

feat(lib): reuse metadata cache across thread-safe scans#7608
Mzack9999 merged 3 commits into
devfrom
dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans

Conversation

@dwisiswant0

@dwisiswant0 dwisiswant0 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Proposed changes

Keep one engine-owned metadata index across SDK
calls and persist it at shutdown instead of
loading and saving a new index for each execution.

Track dirty state and parser validation so
compatible metadata can skip redundant filtering
parses w/o trusting lax entries in strict mode.
Defensively copy cached values, invalidate stale
entries synchronously, and remove the unused
ephemeral engine allocation.

Fixes #7569

Proof

$ git checkout dev
Switched to branch 'dev'
Your branch is up to date with 'origin/dev'.
$ git cherry-pick 6b66f911
[dev c1899d3d] test(lib): add BenchmarkExecuteNucleiWithOptsCtx
 Date: Sun Jul 26 11:10:07 2026 +0700
 1 file changed, 73 insertions(+)
 create mode 100644 lib/multi_bench_test.go
$ go test -run=^$ -bench ^BenchmarkExecuteNucleiWithOptsCtx$ -benchmem -count=10 ./lib | tee BenchmarkExecuteNucleiWithOptsCtx.dev
goos: linux
goarch: amd64
pkg: github.com/projectdiscovery/nuclei/v3/lib
cpu: 11th Gen Intel(R) Core(TM) i9-11900H @ 2.50GHz
BenchmarkExecuteNucleiWithOptsCtx-4   	       3	 464549933 ns/op	160939933 B/op	  966618 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 621234790 ns/op	160889016 B/op	  966967 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 561086810 ns/op	160999428 B/op	  967191 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 587178702 ns/op	160962020 B/op	  966844 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 578571095 ns/op	160964332 B/op	  966553 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 633705634 ns/op	160941108 B/op	  966697 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 684600202 ns/op	160971252 B/op	  966449 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	 959707698 ns/op	160944300 B/op	  966290 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	1074911606 ns/op	160910988 B/op	  967006 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	       2	1040834544 ns/op	160968280 B/op	  966882 allocs/op
PASS
ok  	github.com/projectdiscovery/nuclei/v3/lib	32.527s
$ git checkout -
Switched to branch 'dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans'
$ go test -run=^$ -bench ^BenchmarkExecuteNucleiWithOptsCtx$ -benchmem -count=10 ./lib | tee BenchmarkExecuteNucleiWithOptsCtx.patch
goos: linux
goarch: amd64
pkg: github.com/projectdiscovery/nuclei/v3/lib
cpu: 11th Gen Intel(R) Core(TM) i9-11900H @ 2.50GHz
BenchmarkExecuteNucleiWithOptsCtx-4   	    1836	    585442 ns/op	  127728 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2098	    569311 ns/op	  127402 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2328	    535007 ns/op	  127445 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    1690	    597662 ns/op	  127398 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2709	    552387 ns/op	  127422 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    3418	    543497 ns/op	  127375 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2887	    567774 ns/op	  127373 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2689	    557659 ns/op	  127368 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2412	    600796 ns/op	  127372 B/op	    1071 allocs/op
BenchmarkExecuteNucleiWithOptsCtx-4   	    2067	    643342 ns/op	  127379 B/op	    1071 allocs/op
PASS
ok  	github.com/projectdiscovery/nuclei/v3/lib	17.531s
$ benchstat BenchmarkExecuteNucleiWithOptsCtx.dev BenchmarkExecuteNucleiWithOptsCtx.patch 
goos: linux
goarch: amd64
pkg: github.com/projectdiscovery/nuclei/v3/lib
cpu: 11th Gen Intel(R) Core(TM) i9-11900H @ 2.50GHz
                           │ BenchmarkExecuteNucleiWithOptsCtx.dev │ BenchmarkExecuteNucleiWithOptsCtx.patch │
                           │                sec/op                 │     sec/op       vs base                │
ExecuteNucleiWithOptsCtx-4                         627470.2µ ± 66%       568.5µ ± 6%  -99.91% (p=0.000 n=10)

                           │ BenchmarkExecuteNucleiWithOptsCtx.dev │ BenchmarkExecuteNucleiWithOptsCtx.patch │
                           │                 B/op                  │      B/op        vs base                │
ExecuteNucleiWithOptsCtx-4                         157180.8Ki ± 0%      124.4Ki ± 0%  -99.92% (p=0.000 n=10)

                           │ BenchmarkExecuteNucleiWithOptsCtx.dev │ BenchmarkExecuteNucleiWithOptsCtx.patch │
                           │               allocs/op               │    allocs/op     vs base                │
ExecuteNucleiWithOptsCtx-4                           966.770k ± 0%       1.071k ± 0%  -99.89% (p=0.000 n=10)

The patched version of ExecuteNucleiWithOptsCtx is dramatically faster and more efficient than the dev version:

Metric dev patch Improvement
Time ~627 ms (±66%) ~0.57 ms (±6%) −99.91%
Memory (B/op) ~153.5 MiB ~124 KiB −99.92%
Allocations ~967 k ~1.07 k −99.89%

The patch reduces runtime, memory usage, and allocations by roughly three orders of magnitude (≈1000× better) with high statistical significance (p=0.000).

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Performance

    • Improved template loading efficiency by reusing validated metadata and avoiding unnecessary repeated parsing.
    • Added caching improvements to support faster repeated and concurrent scans.
  • Reliability

    • Strengthened metadata validation to ensure templates are rechecked when stricter validation is required.
    • Improved cache persistence and recovery, including safer handling of updates, stale entries, and concurrent saves.
  • Compatibility

    • Enhanced thread-safe execution with shared metadata caching while preserving existing behavior for standard execution modes.

Signed-off-by: Dwi Siswanto <git@dw1.io>
Keep one engine-owned metadata index across SDK
calls and persist it at shutdown instead of
loading and saving a new index for each execution.

Track dirty state and parser validation so
compatible metadata can skip redundant filtering
parses w/o trusting lax entries in strict mode.
Defensively copy cached values, invalidate stale
entries synchronously, and remove the unused
ephemeral engine allocation.

Signed-off-by: Dwi Siswanto <git@dw1.io>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds validation-aware metadata caching, defensive index persistence, shared metadata indexes for loaders, and lazy thread-safe engine integration. Execution setup reuses the engine index, while new tests and a benchmark cover cache correctness, parser compatibility, concurrency, and repeated execution.

Changes

Metadata cache and execution integration

Layer / File(s) Summary
Validation-aware metadata cache
pkg/catalog/index/metadata.go, pkg/catalog/index/index.go, pkg/catalog/index/index_test.go
Metadata records parser validation mode and is defensively cloned. Index persistence now tracks dirty state, rejects conflicting loads, avoids unchanged writes, invalidates stale entries synchronously, and has expanded concurrency tests.
Shared index and cached template loading
pkg/catalog/loader/loader.go, pkg/catalog/loader/loader_test.go
Loaders accept borrowed metadata indexes, reuse entries compatible with parser settings, refresh incompatible entries, and preserve parsing errors and tag/condition filtering behavior.
Thread-safe engine integration and execution benchmark
lib/sdk.go, lib/multi.go, lib/multi_bench_test.go
Thread-safe engines lazily initialize and save a metadata index, execution setup passes it to loaders, ephemeral setup no longer stores an engine, and repeated execution is benchmarked.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant NucleiEngine
  participant LoaderStore
  participant MetadataIndex
  Client->>NucleiEngine: ExecuteNucleiWithOptsCtx
  NucleiEngine->>MetadataIndex: getMetadataIndex
  NucleiEngine->>LoaderStore: create with MetadataIndex
  LoaderStore->>MetadataIndex: reuse or update template metadata
  LoaderStore-->>NucleiEngine: loaded templates
  NucleiEngine-->>Client: execution result
  Client->>NucleiEngine: Close
  NucleiEngine->>MetadataIndex: Save
Loading

Suggested reviewers: mzack9999, mzack9999

Poem

I’m a rabbit with cache in my burrow tonight,
Validation keeps every template just right.
Clones guard the carrots, locks steady and bright,
Shared indexes hop through each execution flight.
“Thump!” says the benchmark—repeat runs take flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: reusing the metadata cache for thread-safe scans.
Linked Issues check ✅ Passed The changes align with #7569 by reusing an engine-owned metadata index across repeated thread-safe executions and preserving thread-safe behavior.
Out of Scope Changes check ✅ Passed The benchmark and cache/index/loader updates all support the memory-reuse fix and do not appear unrelated to the stated objective.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans

Comment @coderabbitai help to get the list of available commands.

@dwisiswant0
dwisiswant0 marked this pull request as ready for review July 26, 2026 04:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/catalog/index/index.go (1)

320-333: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

GetAll/FilterFunc don't invalidate stale entries like Get now does.

Get synchronously drops entries that fail IsValid() (lines 117-129), but GetAll (and FilterFunc, which is built on GetAll) return cached metadata without any staleness check, so bulk reads can surface metadata for files whose ModTime has changed on disk. This creates an inconsistency between per-key and bulk access paths introduced by this PR's freshness guarantee.

♻️ Possible fix: route GetAll through the same validity check as Get
 func (i *Index) GetAll() map[string]*Metadata {
 	i.mu.RLock()
 	defer i.mu.RUnlock()
 
 	result := make(map[string]*Metadata, i.cache.EstimatedSize())
 	for path, metadata := range i.cache.All() {
-		result[path] = metadata.clone()
+		if metadata.IsValid() {
+			result[path] = metadata.clone()
+		}
 	}
 
 	return result
 }

Also applies to: 349-359

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/catalog/index/index.go` around lines 320 - 333, Update GetAll to apply
the same IsValid() freshness check and remove stale entries synchronously,
matching Get’s behavior; ensure FilterFunc continues using the corrected GetAll
result so both bulk access paths exclude metadata whose files have changed on
disk.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/sdk.go`:
- Around line 268-272: Update closeInternal to obtain the metadata index through
the existing synchronized getMetadataIndex() accessor before saving it, rather
than reading e.metadataIndex directly. Preserve the current nil check and
warning behavior while ensuring the read uses the same synchronization as
metadataIndexOnce.Do.

---

Nitpick comments:
In `@pkg/catalog/index/index.go`:
- Around line 320-333: Update GetAll to apply the same IsValid() freshness check
and remove stale entries synchronously, matching Get’s behavior; ensure
FilterFunc continues using the corrected GetAll result so both bulk access paths
exclude metadata whose files have changed on disk.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d44e96f-53a1-4c7f-8df0-9afcf6c292ed

📥 Commits

Reviewing files that changed from the base of the PR and between 6ecdb94 and f239d6e.

📒 Files selected for processing (8)
  • lib/multi.go
  • lib/multi_bench_test.go
  • lib/sdk.go
  • pkg/catalog/index/index.go
  • pkg/catalog/index/index_test.go
  • pkg/catalog/index/metadata.go
  • pkg/catalog/loader/loader.go
  • pkg/catalog/loader/loader_test.go

Comment thread lib/sdk.go
Comment on lines +268 to +272
if e.metadataIndex != nil {
if err := e.metadataIndex.Save(); err != nil {
e.Logger.Warning().Msgf("Could not save metadata cache: %v", err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unsynchronized read of e.metadataIndex races with concurrent getMetadataIndex() writers.

e.metadataIndex is written inside metadataIndexOnce.Do(...) (lines 107-123), but closeInternal reads the raw field directly instead of going through the same sync.Once-guarded getter. If Close() runs concurrently with an in-flight ExecuteNucleiWithOptsCtx call that's still initializing the index (a realistic scenario for a "thread-safe" engine), this is a data race per the Go memory model.

🔒 Proposed fix: read through the synchronized getter
-	if e.metadataIndex != nil {
-		if err := e.metadataIndex.Save(); err != nil {
+	if metadataIndex := e.getMetadataIndex(); metadataIndex != nil {
+		if err := metadataIndex.Save(); err != nil {
 			e.Logger.Warning().Msgf("Could not save metadata cache: %v", err)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if e.metadataIndex != nil {
if err := e.metadataIndex.Save(); err != nil {
e.Logger.Warning().Msgf("Could not save metadata cache: %v", err)
}
}
if metadataIndex := e.getMetadataIndex(); metadataIndex != nil {
if err := metadataIndex.Save(); err != nil {
e.Logger.Warning().Msgf("Could not save metadata cache: %v", err)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/sdk.go` around lines 268 - 272, Update closeInternal to obtain the
metadata index through the existing synchronized getMetadataIndex() accessor
before saving it, rather than reading e.metadataIndex directly. Preserve the
current nil check and warning behavior while ensuring the read uses the same
synchronization as metadataIndexOnce.Do.

@Mzack9999
Mzack9999 merged commit 4d4b88f into dev Jul 26, 2026
19 checks passed
@Mzack9999
Mzack9999 deleted the dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans branch July 26, 2026 07:59
@Mzack9999 Mzack9999 added the Type: Bug Inconsistencies or issues which will cause an issue or problem for users or implementors. label Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Inconsistencies or issues which will cause an issue or problem for users or implementors.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ThreadSafeNucleiEngine accumulates memory across ExecuteNucleiWithOptsCtx calls (not released until Close)

2 participants