Skip to content

Add progressive subtree caching and singleflight to IPFS tree resolution#92

Closed
hubsmoke wants to merge 1 commit into
developfrom
claude/fix-ipfs-query-performance-euUrT
Closed

Add progressive subtree caching and singleflight to IPFS tree resolution#92
hubsmoke wants to merge 1 commit into
developfrom
claude/fix-ipfs-query-performance-euUrT

Conversation

@hubsmoke

@hubsmoke hubsmoke commented Feb 4, 2026

Copy link
Copy Markdown
Member

Summary

Improves IPFS tree resolution reliability and performance by introducing three complementary caching and concurrency strategies: per-CID DAG node caching, progressive subtree caching with DFS traversal, and singleflight to prevent concurrent duplicate requests.

Key Changes

  • Per-CID DAG node cache: Added caching for individual DAG nodes fetched via fetchDagNode() with a 4-hour TTL. This avoids redundant IPFS HTTP calls when retries occur, improving resilience to transient failures.

  • Progressive subtree caching: Implemented DFS (depth-first) tree traversal that completes subtrees before exploring sibling branches. Completed subtrees are cached individually, allowing partial progress to be preserved even if the full tree resolution times out. On retry, previously-resolved subtrees are grafted directly from cache rather than re-traversed.

  • Singleflight mechanism: Added inflightTrees map to coalesce concurrent requests for the same tree. If a request is already resolving a tree, subsequent requests join the in-flight promise instead of spawning duplicate worker sets. This prevents worker stacking on retries.

  • Subtree completion tracking: Introduced pendingChildren and parentOf maps to track when directory subtrees are fully resolved. Completion propagates upward, triggering subtree caching at each level.

  • Path fixing for cached subtrees: Added fixSubtreePaths() utility to adjust absolute paths when grafting cached subtrees at new positions in the tree (CIDs are content-addressed, so structure is identical but paths depend on mount location).

  • Redis del() method: Extended RedisService interface with a del() method to clean up now-redundant subtree cache entries once the full tree is successfully cached.

Implementation Details

  • Changed queue traversal from FIFO (index-based) to LIFO (stack-based pop()) to enforce DFS order
  • Subtree cache entries are cleaned up after full-tree caching succeeds, avoiding redundant storage
  • Cache reads/writes are non-blocking (wrapped in .catch()) to prevent failures from blocking resolution
  • Enhanced logging to distinguish between full-tree cache skips and partial progress preservation

https://claude.ai/code/session_01Fr42CdDyhXSJCU4b7CtFNt

Summary by CodeRabbit

  • Chores
    • Optimized IPFS content loading with improved caching strategy and better handling of simultaneous requests.
    • Enhanced cache management infrastructure.

…sal, and progressive caching

Three root causes of the myst query fragility:
1. Concurrent requests for the same tree spawn duplicate worker pools
2. BFS traversal prevents caching until the entire tree resolves
3. Only caching fully-resolved trees means timed-out requests make zero progress

Changes:
- Singleflight deduplication: concurrent requests for the same tree share a
  single resolution promise instead of spawning independent worker pools
- DFS traversal (LIFO stack): subtrees complete before sibling branches,
  enabling progressive caching of completed portions
- Subtree completion tracking: each directory tracks pending children;
  when a subtree fully resolves, it's cached individually by CID
- Subtree grafting: on retry, workers check for previously-cached subtrees
  and graft them directly, skipping re-traversal
- Per-CID DAG node caching: individual fetchDagNode responses are cached
  in Redis (4h TTL), making any retry cheaper at the node level
- Subtree cleanup: when the full tree is successfully cached, redundant
  subtree entries are deleted from Redis

https://claude.ai/code/session_01Fr42CdDyhXSJCU4b7CtFNt
@coderabbitai

coderabbitai Bot commented Feb 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The pull request introduces per-CID caching for DAG nodes and subtrees, implements a singleflight mechanism to coalesce concurrent tree resolutions, and refactors tree resolution to use depth-first search with progressive subtree caching. Additionally, a new del method is added to the RedisService API.

Changes

Cohort / File(s) Summary
IPFS Folder Tree Resolution & Caching
src/api/v2/data/getIpfsFolder.ts
Introduced per-CID DAG node and subtree caching with separate TTLs. Added singleflight mechanism (inflightTrees) to prevent worker stacking on concurrent resolutions. Implemented new resolveIpfsTree internal function with DFS-based traversal, progressive subtree caching, cache-based subtree grafting with path correction via fixSubtreePaths, and detailed completion tracking. Enhanced fetchDagNode with pre-fetch Redis reads and post-fetch caching. Added depth-aware enqueueChildren and refined error handling and logging.
Redis Service API Enhancement
src/redis.ts
Added new public del method to RedisService interface and implementation. Method includes readiness check and logs operations.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Caller
    participant SF as Singleflight<br/>(inflightTrees)
    participant Cache as Redis Cache
    participant Resolver as resolveIpfsTree<br/>(DFS Worker)
    participant IPFS as IPFS/DAG<br/>Fetcher
    
    Client->>SF: getIpfsFolderTreeByCid(cid)
    alt In-flight resolution exists
        SF-->>Client: Join existing promise
    else New resolution
        SF->>Resolver: Start DFS resolution
        activate Resolver
        Resolver->>Cache: Check subtree cache
        alt Subtree cached
            Cache-->>Resolver: Return cached subtree
            Resolver->>Resolver: fixSubtreePaths(entry, basePath)
        else Subtree not cached
            Resolver->>IPFS: fetchDagNode(cid)
            IPFS->>Cache: Check DAG node cache
            alt DAG node cached
                Cache-->>IPFS: Return cached node
            else DAG node not cached
                IPFS->>IPFS: Fetch from gateway/fallback
                IPFS->>Cache: Cache DAG node result
            end
            IPFS-->>Resolver: DAG node data
            Resolver->>Resolver: DFS traverse children<br/>(LIFO worker pool)
            Resolver->>Cache: Cache completed subtree
        end
        Resolver->>Cache: Cleanup redundant entries
        deactivate Resolver
        Resolver-->>SF: Return full tree
    end
    SF-->>Client: Return IpfsEntry tree
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • fix mem usage #64: Modifies public-gateway fetch behavior in DAG node fetching within getIpfsFolder.ts, directly impacting the fallback mechanism extended by this PR's caching layer.
  • hybrid data api lookup #60: Alters tree traversal and fetch fallback logic in getIpfsFolder.ts, creating potential interaction points with the new singleflight and caching mechanisms introduced here.

Poem

🐰 Hops through the cache with glee,
No worker stacks—just sweet singflight's spree!
Subtrees grafted, paths made right,
DFS dancing through the night. 🌳✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main changes: introducing progressive subtree caching and singleflight mechanism for IPFS tree resolution, which aligns with the primary performance improvements described in the PR objectives.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/fix-ipfs-query-performance-euUrT

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: 0

Caution

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

⚠️ Outside diff range comments (1)
src/api/v2/data/getIpfsFolder.ts (1)

472-490: ⚠️ Potential issue | 🟡 Minor

Directory entries use parent's gateway instead of the gateway that fetched them.

Line 479 assigns gateway: (item.parent as EnhancedIpfsEntry).gateway, but the directory's DAG node was fetched at line 471 and has its own gateway property. This is inconsistent with file entries (line 498) which correctly use dagNode.gateway.

If the parent was fetched from a fallback gateway but this directory's DAG was fetched from the primary gateway, the entry would incorrectly reflect the fallback.

🔧 Suggested fix
                     const dirEntry: EnhancedIpfsEntry = {
                         name: item.linkName,
                         path: item.path,
                         cid: item.cid,
                         type: "directory",
                         children: [],
-                        gateway: (item.parent as EnhancedIpfsEntry).gateway,
+                        gateway: dagNode.gateway,
                     };
🧹 Nitpick comments (2)
src/api/v2/data/getIpfsFolder.ts (2)

183-189: Silent error swallowing on cache writes is inconsistent with other cache operations.

Cache write failures here are completely swallowed with .catch(() => {}), while similar operations elsewhere (e.g., line 388, line 545) include logging. Consider adding a debug/warn log for consistency and to aid troubleshooting.

🔧 Suggested improvement
                 const result = { ...data, gateway: chosenGateway } as EnhancedIpfsEntry;
                 if (redisService) {
                     void redisService
                         .setToCache(getKeyForDagNode(arg), result, DAG_NODE_CACHE_TTL)
-                        .catch(() => {});
+                        .catch((error) => {
+                            logger.debug({ error, cid: arg }, "Failed to cache DAG node");
+                        });
                 }
                 return result;

Apply the same pattern at line 266:

         if (redisService) {
-            void redisService.setToCache(getKeyForDagNode(arg), result, DAG_NODE_CACHE_TTL).catch(() => {});
+            void redisService.setToCache(getKeyForDagNode(arg), result, DAG_NODE_CACHE_TTL).catch((error) => {
+                logger.debug({ error, cid: arg }, "Failed to cache DAG node from public gateway");
+            });
         }

375-398: The fallback ?? 1 in pending count lookup may mask bugs.

Line 377 uses (pendingChildren.get(parent) ?? 1) - 1, which would result in 0 if the parent was never registered. While the current code paths appear to always register parents before their children resolve, this defensive fallback could silently trigger premature completion if a bug is introduced later.

Consider either asserting the parent exists (fail-fast), or logging when the fallback is used:

🛡️ Alternative: fail-fast or log on unexpected state
     const onChildResolved = (parent: IpfsEntry, childHadError: boolean) => {
         if (childHadError) subtreeHasError.add(parent);
-        const remaining = (pendingChildren.get(parent) ?? 1) - 1;
+        const current = pendingChildren.get(parent);
+        if (current === undefined) {
+            logger.warn({ parentCid: parent.cid, parentPath: parent.path }, "onChildResolved called for untracked parent");
+            return;
+        }
+        const remaining = current - 1;
         pendingChildren.set(parent, remaining);

@m0ar m0ar closed this Feb 9, 2026
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.

3 participants