Skip to content

fix(hardening): TTL-based eviction for connection driver cache (#574) - #582

Merged
alfredo1996 merged 6 commits into
release/2.0from
fix/issue-574-driver-cache-eviction
Apr 20, 2026
Merged

fix(hardening): TTL-based eviction for connection driver cache (#574)#582
alfredo1996 merged 6 commits into
release/2.0from
fix/issue-574-driver-cache-eviction

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds TTL-based eviction (30 min idle) to the connection module cache in query-executor.ts
  • Periodic sweep every 5 minutes cleans up stale driver instances
  • New closeConnection() for explicit cleanup on credential rotation/deletion
  • New closeAllConnections() for tests and graceful shutdown
  • Timer uses unref() so it doesn't prevent Node.js process exit

Test plan

  • 4 new cache eviction tests (closeConnection, closeAll, no-op for unknown, reuse refreshes TTL)
  • All 23 existing query-executor tests pass
  • Build passes (type-check clean)

Closes #574

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added connection lifecycle management APIs to close individual or all cached database connections.
  • Tests

    • Expanded test suite to cover cache eviction behaviors, connection closing scenarios, and module reuse patterns.

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@alfredo1996 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 21 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 140a6af8-7d31-49ef-ba6c-e899d69d9d50

📥 Commits

Reviewing files that changed from the base of the PR and between d2b9432 and 27743af.

📒 Files selected for processing (2)
  • app/src/lib/__tests__/query/query-executor-core.test.ts
  • app/src/lib/query/query-executor.ts

Walkthrough

The PR implements TTL-based cache eviction in the query-executor module, replacing the unbounded moduleCache with one that tracks last access time, evicts stale entries after a configurable idle period, and provides explicit lifecycle management APIs (closeConnection, closeAllConnections) to gracefully close driver instances before removal.

Changes

Cohort / File(s) Summary
Cache Eviction & Lifecycle APIs
app/src/lib/query/query-executor.ts
Replaced unbounded moduleCache with TTL-based cache storing { module, lastAccessedAt }. Added periodic eviction timer that closes stale modules (via close() if available) after CACHE_TTL_MS inactivity. Introduced closeConnection(type, credentials) and closeAllConnections() lifecycle APIs for explicit cache management, plus _getCacheSize() for testing.
Cache Lifecycle Tests
app/src/lib/query/__tests__/query-executor-core.test.ts
Expanded test coverage for cache eviction behaviors including single connection closure, cache clearing, error tolerance for missing/rejecting close() methods, and cache hit reuse validation. Updated connection module mocks to include async close() method.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #507: Modifies moduleCache cache key structure (adds credentials.maxRows) while this PR replaces the entire cache mechanism with TTL-based eviction.
  • #463: Implements close() method on Neo4j connection modules, enabling the new eviction and lifecycle APIs to safely shut down drivers.

Suggested labels

pkg:connection, area:connectors

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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 accurately summarizes the main change: implementing TTL-based eviction for the connection driver cache to address the leak issue in query-executor.
Linked Issues check ✅ Passed The PR implementation meets all coding requirements from issue #574: TTL-based eviction (default 30min), closeConnection() API, closeAllConnections() API, and comprehensive tests for eviction behavior.
Out of Scope Changes check ✅ Passed All changes directly address the cache leak issue: TTL implementation, eviction timer, lifecycle APIs, and tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-574-driver-cache-eviction

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/lib/__tests__/query/query-executor-core.test.ts`:
- Around line 556-569: The test currently only proves cache reuse but not TTL
refresh; update the test to use fake timers (jest.useFakeTimers /
jest.setSystemTime or jest.advanceTimersByTime) and a known TTL value to
exercise the lastAccessedAt refresh path: create the cache entry by calling
executeQuery("neo4j", neo4jCreds, { query: "RETURN 1" }), advance the fake clock
past the configured TTL expiry, then call executeQuery again for "RETURN 2" and
assert the entry was not evicted (expect(_getCacheSize()).toBe(1) and
mockCreateConnectionModule called once). Ensure you restore real timers at the
end and reference the cache entry behavior (entry.lastAccessedAt) by advancing
time before/after the second access so the test fails if lastAccessedAt is not
updated.

In `@app/src/lib/query/query-executor.ts`:
- Around line 54-55: Replace the hard-coded CACHE_TTL_MS and
EVICTION_INTERVAL_MS constants with values read from validated server-side
configuration (e.g., process.env or your config loader) while keeping the
current numeric expressions as fallbacks; locate the constants named
CACHE_TTL_MS and EVICTION_INTERVAL_MS in query-executor.ts and change them to
parse/validate the configured TTL and eviction interval (ensure numeric,
positive, and clamp to sensible min/max) before using them so the idle TTL and
sweep cadence become configurable but safe.
- Around line 103-112: closeConnection currently returns void, deletes the cache
entry and fire-and-forgets closeModuleSilently, so change closeConnection to
return Promise<void> and perform an awaited best-effort close before deleting
the cache entry: use getCacheKey to locate the entry in moduleCache, if found
call an async helper (implement closeModuleBestEffort similar to the example
that checks for a .close function and awaits it inside try/catch) and await it,
then delete the key from moduleCache; after deletion, if moduleCache.size === 0
also stop the background sweep timer (clear the interval variable you use for
sweeping) to avoid leaving it running. Ensure callers can await
closeConnection's returned promise.
- Around line 172-187: The cache is keyed without the password so
rotated/changed credentials reuse a stale driver in moduleCache; update the
PATCH handler that performs credential updates to call closeConnection(type,
newCredentials) immediately after the credentials are saved (so
createConnectionModule will rebuild with new creds next time), and update the
DELETE handler to call closeConnection(type, decryptedCredentials) before
returning confirmation so the removed connection's module is torn down; also
ensure prefetchSchema (which currently runs without invalidating moduleCache)
either calls closeConnection(type, credentials) when it detects credential
changes or explicitly invalidates the moduleCache entry keyed by
getCacheKey(type, credentials) so stale modules are not reused.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f9e42f4-edcd-4639-b916-69a409a59760

📥 Commits

Reviewing files that changed from the base of the PR and between cf2cd73 and d2b9432.

📒 Files selected for processing (2)
  • app/src/lib/__tests__/query/query-executor-core.test.ts
  • app/src/lib/query/query-executor.ts

Comment thread app/src/lib/__tests__/query/query-executor-core.test.ts
Comment on lines +54 to +55
const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
const EVICTION_INTERVAL_MS = 5 * 60 * 1000; // sweep every 5 minutes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the TTL configurable, not just defaulted.

The issue objective calls for a configurable idle TTL, but these constants hard-code both TTL and sweep cadence. Consider reading validated server-side env/config values with these as fallbacks.

Example direction
+function readPositiveDurationMs(name: string, fallbackMs: number): number {
+  const value = Number(process.env[name]);
+  return Number.isFinite(value) && value > 0 ? value : fallbackMs;
+}
+
-const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
-const EVICTION_INTERVAL_MS = 5 * 60 * 1000; // sweep every 5 minutes
+const CACHE_TTL_MS = readPositiveDurationMs(
+  "QUERY_EXECUTOR_CACHE_TTL_MS",
+  30 * 60 * 1000,
+);
+const EVICTION_INTERVAL_MS = readPositiveDurationMs(
+  "QUERY_EXECUTOR_EVICTION_INTERVAL_MS",
+  5 * 60 * 1000,
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/lib/query/query-executor.ts` around lines 54 - 55, Replace the
hard-coded CACHE_TTL_MS and EVICTION_INTERVAL_MS constants with values read from
validated server-side configuration (e.g., process.env or your config loader)
while keeping the current numeric expressions as fallbacks; locate the constants
named CACHE_TTL_MS and EVICTION_INTERVAL_MS in query-executor.ts and change them
to parse/validate the configured TTL and eviction interval (ensure numeric,
positive, and clamp to sensible min/max) before using them so the idle TTL and
sweep cadence become configurable but safe.

Comment thread app/src/lib/query/query-executor.ts
Comment thread app/src/lib/query/query-executor.ts
@alfredo1996
alfredo1996 changed the base branch from dev to release/2.0 April 20, 2026 12:58
alfredorubin96 and others added 3 commits April 20, 2026 15:01
The moduleCache Map had no eviction — every unique credential set
created a driver instance that was never removed. Long-running servers
leaked Neo4j/PG connections on credential rotation or deletion.

Adds TTL-based eviction (30 min idle), a periodic sweep (5 min), and
closeConnection()/closeAllConnections() for explicit cleanup.

Closes #574

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies close() is called on eviction/cleanup, handles modules without
close(), and handles close() rejection gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alfredo1996
alfredo1996 force-pushed the fix/issue-574-driver-cache-eviction branch from d2b9432 to 1d89ffb Compare April 20, 2026 13:01
alfredorubin96 and others added 3 commits April 20, 2026 15:36
The mock factory's inferred type requires close. Coverage of the
close-less path is redundant with the no-op test above.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exports evictStaleEntries as _evictStaleEntries and adds 4 direct tests
using vi.useFakeTimers: empty cache no-op, entries within TTL preserved,
entries past TTL evicted with close(), close() rejection handled.

Simplifies startEvictionTimer's unref() detection via optional chaining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SonarCloud C Reliability: the try/catch is redundant since close() returns
a Promise, and .catch() already handles rejection. Synchronous throw from
close() would only happen if the module is malformed — callers are already
defensive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@alfredo1996
alfredo1996 merged commit 79bd65c into release/2.0 Apr 20, 2026
13 checks passed
@alfredo1996
alfredo1996 deleted the fix/issue-574-driver-cache-eviction branch April 20, 2026 15:47
alfredo1996 added a commit that referenced this pull request Apr 22, 2026
…#594)

Security: enforce canWrite on reassign route, fix invalid 'owner' share
role in SQL, wrap jsonb_agg with COALESCE for NULL safety, add WITH
ORDINALITY to preserve page order, sanitize plugin codegen inputs.

Bugs: extract render helpers in widget-preview-panel to reduce cognitive
complexity from 50 to within limits (S3358 nested ternaries).

Code smells: mark props Readonly (S6759), flip negated condition (S7735),
remove unnecessary type assertions (S4325), evict cached connections on
credential update/delete (#582).

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(hardening): connection driver cache leak in query-executor

2 participants