Skip to content

Conversation

@jschfflr
Copy link
Contributor

@jschfflr jschfflr commented Oct 10, 2025

📝 Summary

Adds full async/await support to marimo's cache decorators (mo.cache, mo.lru_cache, mo.persistent_cache) with automatic task
deduplication to prevent race conditions and duplicate work.

🔍 Description of Changes

What's New

All marimo cache decorators now work seamlessly with both synchronous and asynchronous functions:

@mo.cache
async def fetch_data(url: str, params: dict) -> dict:
    response = await http_client.get(url, params=params)
    return response.json()

@mo.persistent_cache
async def compute_embedding(data: str, model: str) -> np.ndarray:
    response = await llm_client.get_embeddings(data, model)
    return response.embeddings

Key Features

Task Deduplication: When multiple concurrent calls are made to a cached async function with the same arguments, only one execution occurs—the rest await the result. This prevents race conditions and duplicate work.

# All 5 calls execute concurrently, but only one actually runs the function
results = await asyncio.gather(
    expensive_async_compute(42),
    expensive_async_compute(42),
    expensive_async_compute(42),
    expensive_async_compute(42),
    expensive_async_compute(42),
)
# All results are identical, but the function only executed once

Implementation Details

Commit 1: Refactoring (cf47fb0)

  • Extracted helper methods _prepare_call_execution() and _finalize_cache_update() from the sync cache implementation
  • No functional changes, just preparation for async support

Commit 2: Async Support (e250061)

  • Changed to use type(self) instead of _cache_call() in get() for proper subclass dispatch
  • Created _cache_call_async subclass that inherits from _cache_call
  • Implemented task deduplication using a class-level WeakKeyDictionary to track pending executions per cache instance
  • Used WeakKeyDictionary to prevent memory leaks and allow garbage collection of unused cache instances
  • Added threading lock to protect shared data structures during concurrent access
  • Added 15 comprehensive async cache tests (100% coverage of new code paths)

Commit 3: Documentation (52b6e2c).

  • Updated docs/api/caching.md with async examples in tab format
  • Added note about async support and task deduplication behavior
  • Updated comparison table with functools.cache

Technical Decisions

  1. WeakKeyDictionary for instance tracking: Prevents memory leaks by allowing garbage collection of unused cache instances.
  2. Strong references for Task objects: While cache instances use weak references, the pending Task objects must be strong references to
    keep them alive while being awaited.
  3. Async-only task deduplication: Following the pattern from the async-lru library, only the async variant implements task deduplication
    since sync code doesn't have concurrent execution issues.
  4. Deadlock prevention: The lock is released before awaiting existing tasks to prevent deadlocks.

Testing

All existing tests continue to pass (100 sync cache tests), plus 15 new async cache tests covering:

  • Basic async caching
  • Task deduplication with concurrent calls
  • Cache invalidation with different arguments
  • Persistent cache with async functions
  • LRU cache with async functions
  • Error handling and edge cases

📋 Checklist

  • I have read the contributor guidelines.
  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • I have added tests for the changes made.
  • I have run the code and verified that it works as expected.

Extract common execution logic into _prepare_call_execution() and
_finalize_cache_update() helper methods. This reduces duplication and
prepares the codebase for async cache support.

- Add _prepare_call_execution() to build execution context
- Add _finalize_cache_update() to save cache results
- Refactor __call__() to use new helpers
Add support for async/await functions with @cache, @lru_cache, and
@persistent_cache decorators. Implements task deduplication to prevent
race conditions when multiple concurrent calls are made with the same
arguments.

Implementation:
- Use type(self) instead of _cache_call() in __get__() for proper subclass dispatch
- Detect async functions and dispatch to _cache_call_async variant
- Implement task deduplication using asyncio.Task caching with WeakKeyDictionary
- Prevent concurrent duplicate executions via _pending_executions dict
- Release lock before awaiting tasks to avoid deadlocks

Testing:
- Add 15 comprehensive async cache tests
- Test concurrent deduplication (5 concurrent calls → 1 execution)
- All 115 tests passing (100 sync + 15 async)
Add documentation for async/await support in cache decorators:
- Add async examples for @cache and @persistent_cache decorators
- Document task deduplication behavior for concurrent async calls
- Update comparison table to show async support advantage over functools.cache
@github-actions
Copy link

github-actions bot commented Oct 10, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@vercel
Copy link

vercel bot commented Oct 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
marimo-docs Ready Ready Preview Comment Oct 10, 2025 7:13pm

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Oct 10, 2025
@mscolnick
Copy link
Contributor

super cool! thanks for the PR. we will give this a review this week.

@jschfflr
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@jschfflr jschfflr changed the title Async cache support Add async cache support Oct 10, 2025
Copy link
Collaborator

@dmadisetti dmadisetti left a comment

Choose a reason for hiding this comment

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

Comments are non-blocking. This looks great! Thanks!

try:
if attempt.hit:
attempt.restore(scope)
return attempt.meta["return"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return attempt.meta["return"]
return attempt.meta.get("return")

| Tracks closed-over variables |||
| Allows unhashable arguments? |||
| Allows Array-like arguments? |||
| Supports async functions? |||
Copy link
Collaborator

Choose a reason for hiding this comment

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

🥳

@mscolnick
Copy link
Contributor

don't worry about the failing playwright tests (that is now fixed on main)

@dmadisetti dmadisetti merged commit 9934ddc into marimo-team:main Oct 10, 2025
34 of 38 checks passed
@jschfflr
Copy link
Contributor Author

Thanks for the quick merge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants