Skip to content

fix(routerlicious-driver): Properly dispose caches when document service is disposed#26196

Open
anthony-murphy-agent wants to merge 7 commits intomicrosoft:mainfrom
anthony-murphy-agent:fix/routerlicious-driver-memory-disposal
Open

fix(routerlicious-driver): Properly dispose caches when document service is disposed#26196
anthony-murphy-agent wants to merge 7 commits intomicrosoft:mainfrom
anthony-murphy-agent:fix/routerlicious-driver-memory-disposal

Conversation

@anthony-murphy-agent
Copy link
Contributor

@anthony-murphy-agent anthony-murphy-agent commented Jan 13, 2026

Summary

  • Fixes memory leak where DocumentService.dispose() was empty and did nothing
  • Adds clear() method to ICache interface for cache cleanup
  • Implements proper disposal chain: DocumentServiceDocumentStorageService → internal storage services
  • Clears blobsShaCache and prefetchCache on dispose
  • Adds expiration (5 days) to blobCache as a safety net against leaks

Background

Customer reported memory issues when using Fluid Framework with SharedTree and Routerlicious driver. Their heap snapshot showed a retention chain:

clearImmediate → timerListMap → onTimeout → readAndParseBlob (closure retaining ~140MB)

The readAndParseBlob closure captures the storage service, which holds the driver's blob cache:

Container Runtime                          Driver
─────────────────                          ──────
readAndParseBlob (closure)                 
    ↓ captures this.storage
readAndParse(storage, id)
    ↓
storage.readBlob(id)  ──────────────────→  ShreddedSummaryDocumentStorageService.readBlob()
                                               ↓
                                           blobCache.get() / blobCache.put()
                                               ↓
                                           Returns cached blob (~140MB)

The root cause: DocumentService.dispose() was completely empty, so even when containers were disposed, the driver caches retained all blob data indefinitely.

Fix

When dispose() is called:

  1. DocumentService.dispose()DocumentStorageService.dispose()
  2. All caches are cleared (blobCache.clear(), snapshotTreeCache.clear(), etc.)
  3. The ~140MB of cached blob data is released

Even if timer callbacks still hold the readAndParseBlob closure reference, the actual blob data in the cache is now freed on dispose.

Changes

File Change
cache.ts Add clear() to ICache interface and implementations
documentService.ts Implement dispose() to call storage service dispose
documentStorageService.ts Add dispose() to propagate to internal service
shreddedSummaryDocumentStorageService.ts Add dispose() to clear caches
wholeSummaryDocumentStorageService.ts Add dispose() to clear caches
prefetchDocumentStorageService.ts Add dispose() to clear prefetchCache
documentServiceFactory.ts Add expiration to blobCache
cache.spec.ts Add test for clear()

Test plan

  • Existing unit tests pass (133 tests)
  • New test for clear() added
  • Manual memory profiling to verify improvement

🤖 Generated with Claude Code

@anthony-murphy-agent anthony-murphy-agent force-pushed the fix/routerlicious-driver-memory-disposal branch 3 times, most recently from 7111f40 to 32833c7 Compare January 13, 2026 17:35
…ice is disposed

Previously, `DocumentService.dispose()` was empty, causing memory leaks:
- Factory-level caches retained all blobs indefinitely
- `blobsShaCache` grew unbounded with each blob read
- `PrefetchDocumentStorageService` cache was never cleared

This change:
- Adds `removeByPrefix()` to ICache for document-specific cleanup
- Implements `dispose()` in DocumentService to propagate to storage
- Adds `dispose()` to storage services to clean document-specific entries
- Adds expiration to blobCache as a safety net
- Clears blobsShaCache and prefetchCache on dispose

This ensures calling `dispose()` properly releases memory regardless of
when it's called (immediately or after the "saved" event).

Co-Authored-By: anthony-murphy <anthony.murphy@microsoft.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@anthony-murphy-agent anthony-murphy-agent force-pushed the fix/routerlicious-driver-memory-disposal branch from 32833c7 to 7da42b1 Compare January 13, 2026 17:39
@anthony-murphy-agent anthony-murphy-agent marked this pull request as ready for review January 13, 2026 17:46
@anthony-murphy
Copy link
Contributor

/azp run Build - api-markdown-documenter, Build - benchmark-tool, Build - build-common, Build - build-tools, Build - client packages, Build - common-utils, Build - eslint-config-fluid, Build - eslint-plugin-fluid, Build - protocol-definitions, Build - test-tools

@anthony-murphy
Copy link
Contributor

/azp run repo-policy-check, server-gitrest, server-gitssh, server-historian, server-routerlicious

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

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

This PR fixes a memory leak where DocumentService.dispose() was empty and did nothing, causing blob caches to retain data indefinitely even after containers were disposed. The fix implements a proper disposal chain and adds cache cleanup methods.

Changes:

  • Added clear() method to ICache interface and its implementations
  • Implemented disposal chain: DocumentServiceDocumentStorageService → internal storage services → cache cleanup
  • Added 5-day expiration to blobCache as a safety net against memory leaks

Reviewed changes

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

Show a summary per file
File Description
cache.ts Adds clear() method to ICache interface and implements it in InMemoryCache and NullCache
documentService.ts Implements dispose() to call documentStorageService?.dispose()
documentStorageService.ts Adds dispose() to propagate disposal to internal storage service
prefetchDocumentStorageService.ts Implements dispose() to clear prefetchCache and propagate to internal service
shreddedSummaryDocumentStorageService.ts Implements dispose() to clear blobCache, snapshotTreeCache, and blobsShaCache
wholeSummaryDocumentStorageService.ts Implements dispose() to clear blobCache and snapshotTreeCache
documentServiceFactory.ts Adds 5-day expiration to blobCache for additional protection
cache.spec.ts Adds unit test for clear() functionality

@anthony-murphy
Copy link
Contributor

/azp run Build - api-markdown-documenter, Build - benchmark-tool, Build - build-common, Build - build-tools, Build - client packages, Build - common-utils, Build - eslint-config-fluid, Build - eslint-plugin-fluid, Build - protocol-definitions, Build - test-tools

@anthony-murphy
Copy link
Contributor

/azp run repo-policy-check, server-gitrest, server-gitssh, server-historian, server-routerlicious

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

anthony-murphy-agent and others added 2 commits February 6, 2026 17:16
The blobCache and snapshotTreeCache are shared across all documents via
the factory, so clearing them in a per-document dispose() would wipe
cached data for all active documents. Only clear the per-instance
blobsShaCache. The MapWithExpiration-based expiration handles cleanup
of the shared caches over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: anthony-murphy <anthony.murphy@microsoft.com>
@anthony-murphy
Copy link
Contributor

/azp run Build - api-markdown-documenter, Build - benchmark-tool, Build - build-common, Build - build-tools, Build - client packages, Build - common-utils, Build - eslint-config-fluid, Build - eslint-plugin-fluid, Build - protocol-definitions, Build - test-tools

@anthony-murphy
Copy link
Contributor

/azp run repo-policy-check, server-gitrest, server-gitssh, server-historian, server-routerlicious

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

anthony-murphy-agent and others added 2 commits February 6, 2026 17:29
The blobCache and snapshotTreeCache are factory-level shared caches.
Using clear() in dispose() wiped cached data for ALL active documents,
not just the one being disposed. This adds removeByPrefix() to the
ICache interface and uses it in dispose() to only remove entries keyed
with this document's ID prefix (`${this.id}:`), leaving other
documents' cache entries intact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: anthony-murphy <anthony.murphy@microsoft.com>
…:anthony-murphy-agent/FluidFramework into fix/routerlicious-driver-memory-disposal
@anthony-murphy-agent
Copy link
Contributor Author

Azure Pipelines commands (copy and post separately, limit of 10 at a time):

/azp run Build - api-markdown-documenter, Build - benchmark-tool, Build - build-common, Build - build-tools, Build - client packages, Build - common-utils, Build - eslint-config-fluid, Build - eslint-plugin-fluid, Build - protocol-definitions, Build - test-tools
/azp run repo-policy-check, server-gitrest, server-gitssh, server-historian, server-routerlicious

@anthony-murphy-agent
Copy link
Contributor Author

Azure Pipelines need to be triggered for this fork PR. Please run the following:

/azp run Build - api-markdown-documenter, Build - benchmark-tool, Build - build-common, Build - build-tools, Build - client packages, Build - common-utils, Build - eslint-config-fluid, Build - eslint-plugin-fluid, Build - protocol-definitions, Build - test-tools
/azp run repo-policy-check, server-gitrest, server-gitssh, server-historian, server-routerlicious

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.

2 participants