Skip to content

feat(connection): add TTL/LRU eviction to ConnectionManager instance cache#404

Merged
polaz merged 3 commits intomainfrom
feat/#376-feat-add-ttllru-eviction-to-connectionmanager-per
Apr 7, 2026
Merged

feat(connection): add TTL/LRU eviction to ConnectionManager instance cache#404
polaz merged 3 commits intomainfrom
feat/#376-feat-add-ttllru-eviction-to-connectionmanager-per

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 7, 2026

Summary

  • Add bounded memory for ConnectionManager.instances Map in high-cardinality multi-instance (OAuth SaaS) scenarios
  • Implement TTL-based expiry and LRU eviction triggered on every new instance insertion
  • Two new env vars: GITLAB_INSTANCE_CACHE_MAX (default: 100) and GITLAB_INSTANCE_TTL_MS (default: 1h)

Technical Details

Problem

In OAuth multi-tenant mode, every unique GitLab URL creates a new InstanceState entry in ConnectionManager.instances. Without a bound, the map grows without limit — in a long-running SaaS server serving many tenants this is a memory leak.

Implementation

evictExpired() — called on every insert; removes entries whose lastAccessedAt exceeds INSTANCE_TTL_MS. Skip rules: currentInstanceUrl and any URL with an in-flight initializePromises or introspectionPromises entry (evicting those would orphan awaiting callers).

evictLRUIfOverCapacity() — called after evictExpired(); removes the least-recently-used (oldest lastAccessedAt) entry until instances.size <= MAX_INSTANCES. Same skip rules apply.

touchInstance(url) — records Date.now() into instanceAccessTimes on every resolveState() call (all read paths) and on initial insertion.

dropInstance(url) — atomically removes from both instances and instanceAccessTimes.

Static guardsMAX_INSTANCES and INSTANCE_TTL_MS use Number.isFinite() fallbacks to stay safe against partial test mocks.

Test Plan

  • 13 new unit tests: TTL expiry, LRU ordering, multi-evict, currentInstanceUrl protection, in-flight promise protection, touchInstance on read, reset() cleanup
  • All 4914 existing tests pass
  • Build clean, lint clean

…cache

- Add GITLAB_INSTANCE_CACHE_MAX (default: 100) and GITLAB_INSTANCE_TTL_MS
  (default: 1h) env vars to cap per-URL InstanceState memory
- Implement evictExpired(): removes entries idle beyond TTL on each insert
- Implement evictLRUIfOverCapacity(): removes oldest-accessed entry when
  over the capacity limit, also triggered on each insert
- Both eviction paths skip currentInstanceUrl and entries with in-flight
  initialize/introspection promises to prevent orphaned callers
- touchInstance() updates LRU timestamp on every resolveState() read
- dropInstance() removes entries from both instances and instanceAccessTimes
- Static MAX_INSTANCES/INSTANCE_TTL_MS use Number.isFinite guard for safety
  against partial test mocks that omit the new config constants
- 13 new unit tests covering: TTL expiry, LRU ordering, multi-evict,
  currentInstanceUrl protection, in-flight promise protection, touch path,
  and reset cleanup
- Update README.md.in with new env var documentation

Closes #376
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 976215c1-b750-497e-b285-86b831f48686

📥 Commits

Reviewing files that changed from the base of the PR and between 09f0e16 and e1f9c53.

📒 Files selected for processing (1)
  • src/services/ConnectionManager.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Configurable per-URL instance cache with adjustable size limit and idle-time eviction to improve connection resilience and memory use.
  • Documentation

    • Added two environment variables to configuration docs to control cache capacity and idle TTL.
  • Tests

    • Added unit tests covering TTL and LRU eviction behavior and cache-access timestamping.

Walkthrough

Adds per-URL InstanceState access tracking and bounded in-memory cache with LRU and idle-TTL eviction in ConnectionManager; introduces two new env-config constants to control capacity and TTL; updates read/initialize/reset paths to touch/protect entries; tests expanded to cover eviction behavior.

Changes

Cohort / File(s) Summary
Documentation
README.md.in
Documented two new environment variables: GITLAB_INSTANCE_CACHE_MAX and GITLAB_INSTANCE_TTL_MS.
Configuration
src/config.ts
Added exported constants GITLAB_INSTANCE_CACHE_MAX (default 100) and GITLAB_INSTANCE_TTL_MS (default 3600000) parsed from env.
Core Implementation
src/services/ConnectionManager.ts
Added instanceAccessTimes tracking plus touchInstance, dropInstance, evictExpired, evictLRUIfOverCapacity; integrated TTL/LRU eviction into initialize/read/error/reset flows; protect current instance and in-flight promises; evict related introspection cache keys on drop.
Tests
tests/unit/services/ConnectionManager.test.ts, tests/unit/services/ConnectionManagerEnhanced.test.ts
Expanded test internals/mocks to expose new statics and access times; added tests for TTL and LRU eviction, protection of current/in-flight entries, touch behavior, and reset clearing access times.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant CM as ConnectionManager
    participant Instances as InstancesMap
    participant Introspect as IntrospectionCache

    Client->>CM: initialize(url)
    CM->>Instances: lookup or create InstanceState
    CM->>CM: touchInstance(url)
    CM->>CM: evictExpired()
    CM->>CM: evictLRUIfOverCapacity()
    alt state exists
        CM->>Client: return client
        Client->>CM: calls getClient(url)
        CM->>Instances: resolveState(url)
        CM->>CM: touchInstance(url)
    else initialization in-flight
        CM->>Client: await initializePromises[url]
    end
    CM->>Introspect: drop related introspection keys on dropInstance(url)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding TTL/LRU eviction to the ConnectionManager instance cache, which is the central feature across all modified files.
Description check ✅ Passed The description provides comprehensive context on the problem (unbounded memory growth), implementation details (evictExpired, evictLRUIfOverCapacity, touchInstance), configuration variables, and test coverage—all directly related to the changeset.

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

✨ 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 feat/#376-feat-add-ttllru-eviction-to-connectionmanager-per

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 7, 2026

Test Coverage Report

Overall Coverage: 96.5%

Metric Percentage
Statements 95.87%
Branches 87.06%
Functions 94.92%
Lines 96.5%

View detailed coverage report

@coderabbitai coderabbitai Bot added the javascript Pull requests that update javascript code label Apr 7, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 94.23077% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/ConnectionManager.ts 94.00% 1 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/services/ConnectionManager.test.ts (1)

67-69: ⚠️ Potential issue | 🟡 Minor

injectWithTime() undoes the "third-party current URL" setup.

injectInstanceState() resets currentInstanceUrl to the injected URL at Lines 67-69, so every call from injectWithTime() overwrites the value established in beforeEach(). That means the last injected entry is still protected by the eviction guards, and several tests are not exercising the unprotected TTL/LRU path their comments describe.

Proposed fix
     function injectWithTime(
       url: string,
       accessedAt: number,
       overrides: Partial<InstanceState> = {},
     ): void {
+      const preservedCurrent = internals(manager).currentInstanceUrl;
       injectInstanceState(manager, url, { isInitialized: true, ...overrides });
       internals(manager).instanceAccessTimes.set(url, accessedAt);
+      internals(manager).currentInstanceUrl = preservedCurrent;
     }

Also applies to: 434-448

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/services/ConnectionManager.test.ts` around lines 67 - 69,
injectInstanceState() is overwriting the test's "third-party current URL" by
unconditionally setting internals(manager).currentInstanceUrl, which causes
injectWithTime() calls to undo the beforeEach() setup; change
injectInstanceState() so it does not reset internals(manager).currentInstanceUrl
unless explicitly required (e.g., add an optional parameter setCurrent:boolean
defaulting to false, or only set currentInstanceUrl when it is currently
undefined), and apply the same change to the other occurrence of
injectInstanceState() used around the 434-448 region so that injectWithTime() no
longer overwrites the test-established currentInstanceUrl.
src/services/ConnectionManager.ts (1)

165-170: ⚠️ Potential issue | 🟠 Major

Refresh access timestamps on every successful cache hit.

touchInstance() currently only runs on insert and through resolveState(). The initialize() cache-hit path, the cached early-return in ensureIntrospected(), getInstanceClient(), and the direct getters at Lines 666-716 all return live state without updating instanceAccessTimes. In multi-instance OAuth traffic, those are hot paths, so a recently used instance can still look idle and be evicted on the next insert.

Also applies to: 429-430, 574-575, 612-619, 666-716

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/ConnectionManager.ts` around lines 165 - 170, Cache HIT paths
are not updating instanceAccessTimes, so call touchInstance(...) on every
successful cache hit to refresh access timestamps: in
ConnectionManager.initialize() where you early-return after finding
existing?.isInitialized, invoke this.touchInstance(url) before setting
this.currentInstanceUrl and returning; likewise add this.touchInstance(url) at
the cached early-return in ensureIntrospected(), inside getInstanceClient() on
the fast-return of a live instance, and at every direct getter that returns live
instance state (the getters that currently return instances without mutation).
Use the existing touchInstance method/signature so cache eviction sees recent
activity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/ConnectionManager.ts`:
- Around line 95-98: dropInstance currently deletes live maps but leaves
ConnectionManager.introspectionCache entries for that URL, allowing unbounded
memory growth; update dropInstance (and the other instance-teardown sites
referenced) to also remove the introspectionCache entries for both the raw URL
key and the introspection endpoint key (`url` and `${url}/api/graphql`), or
alternatively implement a TTL/LRU eviction for
ConnectionManager.introspectionCache and ensure teardown paths call that
eviction; locate and modify the dropInstance method and the other
instance-removal spots (the blocks that call this.instances.delete /
this.instanceAccessTimes.delete) to call introspectionCache.delete(key) for both
keys (or invoke the new TTL/LRU cleanup) so schema payloads are removed when an
instance is dropped.

---

Outside diff comments:
In `@src/services/ConnectionManager.ts`:
- Around line 165-170: Cache HIT paths are not updating instanceAccessTimes, so
call touchInstance(...) on every successful cache hit to refresh access
timestamps: in ConnectionManager.initialize() where you early-return after
finding existing?.isInitialized, invoke this.touchInstance(url) before setting
this.currentInstanceUrl and returning; likewise add this.touchInstance(url) at
the cached early-return in ensureIntrospected(), inside getInstanceClient() on
the fast-return of a live instance, and at every direct getter that returns live
instance state (the getters that currently return instances without mutation).
Use the existing touchInstance method/signature so cache eviction sees recent
activity.

In `@tests/unit/services/ConnectionManager.test.ts`:
- Around line 67-69: injectInstanceState() is overwriting the test's
"third-party current URL" by unconditionally setting
internals(manager).currentInstanceUrl, which causes injectWithTime() calls to
undo the beforeEach() setup; change injectInstanceState() so it does not reset
internals(manager).currentInstanceUrl unless explicitly required (e.g., add an
optional parameter setCurrent:boolean defaulting to false, or only set
currentInstanceUrl when it is currently undefined), and apply the same change to
the other occurrence of injectInstanceState() used around the 434-448 region so
that injectWithTime() no longer overwrites the test-established
currentInstanceUrl.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: fa46e6ae-1518-4d9c-a89d-2281e48402b8

📥 Commits

Reviewing files that changed from the base of the PR and between 713d23b and 9470967.

📒 Files selected for processing (5)
  • README.md.in
  • src/config.ts
  • src/services/ConnectionManager.ts
  • tests/unit/services/ConnectionManager.test.ts
  • tests/unit/services/ConnectionManagerEnhanced.test.ts

Comment thread src/services/ConnectionManager.ts
…cache-hit paths

- dropInstance now removes both introspectionCache keys (bare URL and
  /api/graphql suffix) preventing schema payload accumulation for evicted
  instances
- touchInstance called on every path that bypasses resolveState:
  initialize() early-return, ensureIntrospected() cache hit,
  getInstanceClient() fallback, and all six direct-getter methods
  (isConnected, isFeatureAvailable, getTier, getVersion,
  isWidgetAvailable, getTokenScopeInfo)
- injectWithTime() preserves currentInstanceUrl from beforeEach so
  eviction tests exercise the unprotected path
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 7, 2026

@coderabbitai Re outside-diff items in latest review. (1) tests/unit/services/ConnectionManager.test.ts:67-69 injectWithTime undoes currentInstanceUrl: Fixed - injectWithTime() now saves and restores currentInstanceUrl around the injectInstanceState() call. (2) src/services/ConnectionManager.ts:165-170 cache-hit paths not touching instanceAccessTimes: Fixed - added touchInstance(url) to initialize() early-return, ensureIntrospected() cache-hit, getInstanceClient() fallback, and all six direct-getter methods.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me quickly verify both fixes in the PR branch.


🏁 Script executed:

#!/bin/bash
# Fix 1: check injectWithTime saves/restores currentInstanceUrl
rg -n "injectWithTime|currentInstanceUrl" tests/unit/services/ConnectionManager.test.ts -A2 -B2 | head -60

echo "---"

# Fix 2: check touchInstance calls in the relevant methods
rg -n "touchInstance" src/services/ConnectionManager.ts -B3 -A1

Length of output: 6091


@polaz Both fixes are confirmed in the branch:

  1. injectWithTime / currentInstanceUrl preservation — Lines 445–448 correctly capture preservedCurrent before injectInstanceState() and restore it afterward, so eviction tests consistently exercise the unprotected TTL/LRU path.

  2. touchInstance coverage — All cache-hit / read paths now call touchInstance(url): initialize() early-return (line 176), doInitialize() insert (line 271), ensureIntrospected() cache-hit (line 439), resolveState() (line 584), getInstanceClient() fallback (line 625), all six direct-getter methods (isConnected, hasFeature, getTier, getVersion, isWidgetAvailable, getTokenScopeInfo — lines 683–734), and the reinitialize() restore path (line 936).

Both look correct — thanks for the thorough follow-up!


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/404

Timestamp: 2026-04-07T17:48:27.054Z
Learning: In `tests/unit/services/ConnectionManager.test.ts` (structured-world/gitlab-mcp), `injectWithTime()` (around line 440) saves `internals(manager).currentInstanceUrl` into `preservedCurrent` before calling `injectInstanceState()`, sets `instanceAccessTimes` for the injected URL, then restores `currentInstanceUrl` to `preservedCurrent`. This prevents the helper from silently protecting the last-injected URL from TTL/LRU eviction in tests.

Learnt from: polaz
URL: https://github.com/structured-world/gitlab-mcp/pull/404

Timestamp: 2026-04-07T17:48:27.054Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `touchInstance(url)` is called on every cache-hit / read path to maintain LRU correctness: `initialize()` early-return (already-initialized guard), `doInitialize()` after new-entry insert, `ensureIntrospected()` cache-hit, `resolveState()`, `getInstanceClient()` fallback, and all six direct-getter methods (`isConnected`, `hasFeature`, `getTier`, `getVersion`, `isWidgetAvailable`, `getTokenScopeInfo`), plus the `reinitialize()` state-restore path. Do not flag these paths as missing LRU updates in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T08:14:19.833Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), private `ConnectionManager` internals are accessed via typed `cmStatic()` and `cmInternal()` helper functions (replacing prior `as any` casts). These helpers expose `instances`, `currentInstanceUrl`, `doInitialize`, and related private fields/methods to set up intermediate states unreachable via the public API. This typed-helper pattern is the accepted approach for this test file. Do not suggest reverting to `as any` or flag the private-field access as unsafe in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T10:01:57.587Z
Learning: In `tests/unit/services/ConnectionManagerEnhanced.test.ts` (structured-world/gitlab-mcp), around line 1079, the `ensureIntrospected` cache-path test is already correct: the `getIntrospection` mock (set up around line 986) returns full cached data, and the test asserts both that `getIntrospection` was called and that the restored introspection data is present. Do not flag this test as missing cache-restoration assertions or as incorrectly set up in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T15:31:04.797Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the stale-init overwrite race in `doInitialize()` (lines ~117-163) is mitigated by identity guards: the outer `let state` binding + `this.instances.get(baseUrl) === state` check in the catch block (lines ~294-298) prevents a stale init from deleting a fresh state entry. A deeper fix using per-attempt init tokens (to prevent any intermediate writes from stale paths) requires significant refactoring across `doInitialize`/`clearInflight`/`reset` and is tracked in issue `#379`. Do not re-flag the stale-init race as unguarded in future reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-03T07:33:01.038Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `refreshTokenScopes()` (around lines 624-651) applies a post-await identity guard: after `await detectTokenScopes(url)` returns, it re-reads `this.instances.get(url)` and compares it to the `state` reference captured before the await. If a concurrent `reinitialize()` or `reset()` replaced the entry, it returns `false` instead of writing stale scope info into the new state. This mirrors the identity guard in `doInitialize`'s catch block. Do not flag the lack of this guard as a stale-write bug in future reviews — it is already in place.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 396
File: src/registry-manager.ts:87-89
Timestamp: 2026-04-06T00:31:02.359Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), the Map fields `instances` and `initializePromises` are intentionally NOT marked `readonly`. The old consistency reason (alignment with `toolLookupCache` which was atomically reassigned) is no longer valid after PR `#396` replaced the singleton `toolLookupCache` with per-URL readonly Maps (`toolLookupCaches`, `toolDefinitionsCaches`, `toolNamesCaches`) in `src/registry-manager.ts`. Do not flag `instances` and `initializePromises` as missing `readonly` in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/HealthMonitor.ts:100-108
Timestamp: 2026-03-29T18:10:49.416Z
Learning: In structured-world/gitlab-mcp, the `ConnectionManager.isConnected()` / `getInstanceInfo()` singleton path in `HealthMonitor.ts` (`performConnect`) is intentional for single-instance mode. Multi-instance OAuth contexts use `InstanceRegistry` for per-URL state. Do not flag this singleton usage as a bug in future reviews; the two modes have separate code paths by design.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T12:32:00.330Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `isWidgetAvailable(widgetType: string, instanceUrl?: string)` (around line 586) accepts an optional `instanceUrl`, resolves it via `normalizeInstanceUrl`, and reads widget availability directly from `state?.schemaInfo?.workItemWidgetTypes` instead of delegating to `schemaIntrospector`'s internal cache — this is intentional because `schemaIntrospector`'s cache may not be populated after a cache-hit restore. Do not flag the direct `schemaInfo` read or the optional `instanceUrl` parameter as incorrect in future reviews.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 380
File: src/services/ConnectionManager.ts:65-67
Timestamp: 2026-04-03T14:50:27.051Z
Learning: In `src/utils/url.ts` (structured-world/gitlab-mcp), `normalizeInstanceUrl()` was updated (after PR `#380` review) to strip default ports (443 for https, 80 for http) using the URL constructor, so `https://host` and `https://host:443` map to the same entry in `ConnectionManager.instances` and inflight caches. When reviewing this function, verify: (1) `new URL()` call is wrapped in try/catch to handle malformed inputs gracefully, (2) API suffix stripping runs before or independently of port normalization to preserve subpaths for self-managed instances, (3) unit tests cover `https://host:443` → `https://host` and `http://host:80` → `http://host` cases.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-05T08:08:46.302Z
Learning: In `tests/unit/utils/fetch.coverage.test.ts` (structured-world/gitlab-mcp), the `Pool` entry in the `jest.mock('undici', ...)` block is intentionally kept: `InstanceConnectionPool.ts` calls `new undici.Pool(...)` and is loaded transitively via `InstanceRegistry`, so removing the `Pool` mock breaks per-instance dispatcher tests. Do not suggest removing or simplifying the `Pool` mock in the undici module mock in future reviews of this test file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-04-02T16:55:06.625Z
Learning: In `src/services/HealthMonitor.ts` (structured-world/gitlab-mcp), `HealthMonitor` does NOT call `InstanceRegistry.getIntrospection()` anywhere. Health and degraded-state detection is derived exclusively from `ConnectionManager.getInstanceInfo(url)` and `ConnectionManager.getSchemaInfo(url)`. Do not suggest testing `InstanceRegistry.getIntrospection()` branches in `HealthMonitor` tests in future reviews — it is not part of the health monitor's code path.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:451-453
Timestamp: 2026-04-02T14:35:05.324Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `resolveState()` currently falls back to `this.currentInstanceUrl` when no `instanceUrl` is passed. Zero-arg callers in `src/services/ToolAvailability.ts` (around lines 339-357, 436-454) and `src/services/WidgetAvailability.ts` (around lines 110-126, 162-177) can therefore read another concurrent OAuth request's version/tier. These callers need to pass an explicit `instanceUrl` from `getGitLabApiUrlFromContext()` to avoid cross-request state leakage. This is tracked as issue `#379` (not fixed in PR `#369`). Do not flag resolveState itself as the sole issue — the callers need updating too.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/services/ConnectionManager.ts:112-112
Timestamp: 2026-04-02T10:46:39.546Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `doInitialize()` (around line 112) has a cognitive complexity of ~34 (SonarCloud limit: 15) due to three interleaved init paths: OAuth best-effort unauthenticated version detection, REST-only token-scope path, and full GraphQL introspection. The maintainer (polaz) has explicitly decided NOT to extract helper methods in PR `#369` because the shared `state` mutations and identity guards would add indirection without reducing conceptual complexity. This is tracked for future refactoring. Do not re-flag this complexity issue in reviews of this file.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 0
File: :0-0
Timestamp: 2026-03-31T07:51:52.620Z
Learning: In `src/services/ConnectionManager.ts` (structured-world/gitlab-mcp), `clearInflight(instanceUrl: string)` (around line 761) removes the inflight `initializePromises` entry for a URL. HealthMonitor calls this after an init timeout so reconnect attempts start a fresh `doInitialize()` instead of re-awaiting the hung promise. The `initialize()` method's `finally` block guards `initializePromises.delete(url)` with an identity check to prevent a retry's fresh promise from being wiped by a stale `clearInflight` call.

Learnt from: polaz
Repo: structured-world/gitlab-mcp PR: 369
File: src/registry-manager.ts:293-307
Timestamp: 2026-03-31T16:09:46.248Z
Learning: In `src/registry-manager.ts` (structured-world/gitlab-mcp), `buildToolLookupCache(instanceUrl?)` intentionally bakes one URL's tier/version/scope view into the single shared `toolLookupCache`. This is an accepted trade-off: the MCP protocol exposes a single `tools/list` per server (not per-session), so per-URL keyed caches would require session-aware tool dispatch — a larger architectural change. In single-instance mode (99% of usage) the cache is always correct. In multi-instance OAuth the last `refreshCache(url)` wins; the handler-level health gate (handlers.ts) prevents cross-instance tool execution regardless of cache state. The limitation is documented in the code comment around lines 296-302. Do not flag this singleton cache as a cross-instance overwrite bug in future reviews.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/services/ConnectionManager.ts`:
- Around line 67-83: Mark the instanceAccessTimes field as readonly because it's
never reassigned (only mutated via .set/.delete/.clear) to satisfy SonarCloud;
keep the static fields MAX_INSTANCES and INSTANCE_TTL_MS unchanged (they must
remain mutable for tests that override them via statics()). Locate the
declarations instanceAccessTimes, MAX_INSTANCES, and INSTANCE_TTL_MS in
ConnectionManager and change only instanceAccessTimes to readonly while leaving
the two static properties as-is.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 08e4b8ea-8466-41b9-8bc4-a0646b8b781a

📥 Commits

Reviewing files that changed from the base of the PR and between 9470967 and 09f0e16.

📒 Files selected for processing (2)
  • src/services/ConnectionManager.ts
  • tests/unit/services/ConnectionManager.test.ts

Comment thread src/services/ConnectionManager.ts Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds bounded-memory behavior to ConnectionManager.instances to prevent unbounded growth in high-cardinality OAuth multi-tenant scenarios by evicting idle entries (TTL) and least-recently-used entries (LRU), with configuration exposed via new environment variables.

Changes:

  • Implement TTL expiry + LRU eviction for cached per-instance connection state in ConnectionManager.
  • Add config/env support for GITLAB_INSTANCE_CACHE_MAX and GITLAB_INSTANCE_TTL_MS.
  • Add/extend unit tests covering eviction behavior and access-time tracking; document the new env vars.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/services/ConnectionManager.ts Adds access-time tracking plus TTL/LRU eviction and integrates “touch” updates on multiple read/insert paths.
src/config.ts Introduces parsed configuration for cache capacity and TTL with safe defaults.
tests/unit/services/ConnectionManager.test.ts Adds comprehensive unit tests for TTL expiry, LRU eviction, protection rules, and reset behavior.
tests/unit/services/ConnectionManagerEnhanced.test.ts Updates config mock to include new exported config constants used by the service.
README.md.in Documents the two new environment variables and defaults.

Comment thread src/services/ConnectionManager.ts Outdated
…reIntrospected paths

- instanceAccessTimes is never reassigned (only .set/.delete/.clear),
  so readonly satisfies SonarCloud without affecting test mutability
- Move touchInstance() before cache-hit check in ensureIntrospected()
  so LRU timestamp is refreshed on every call path (cache-hit, await
  existing introspection, and new introspection start)
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 7, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@polaz polaz merged commit 229c92c into main Apr 7, 2026
24 checks passed
@polaz polaz deleted the feat/#376-feat-add-ttllru-eviction-to-connectionmanager-per branch April 7, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants