Singleflight¶
While a call for a given key is running, further calls with the same key wait and receive the same result or exception. Once it finishes, the next call runs fresh.
This de-duplicates work in flight; it is not a cache. Use it to stop a thundering herd of identical requests (a cache miss that every request tries to fill).
import time
from concurrent.futures import ThreadPoolExecutor
from python_wrap import singleflight
calls = []
@singleflight
def load(path: str) -> str:
calls.append(path)
time.sleep(0.03)
return f"contents of {path}"
with ThreadPoolExecutor(3) as pool:
results = list(pool.map(load, ["a.txt"] * 3))
assert results == ["contents of a.txt"] * 3
assert len(calls) == 1
import asyncio
from python_wrap import singleflight
calls = []
@singleflight
async def load(path: str) -> str:
calls.append(path)
await asyncio.sleep(0.01)
return f"contents of {path}"
async def main() -> None:
results = await asyncio.gather(*(load("a.txt") for _ in range(3)))
assert results == ["contents of a.txt"] * 3
assert calls == ["a.txt"]
asyncio.run(main())
When to use it¶
- Many callers ask for the same expensive thing at the same moment.
- Stacked under
memoize, so a cache miss is filled once rather than by every concurrent caller. - Not to remember results after the call finishes: use
memoize. - Not to merge different items into one bulk call: use
batch_calls.
Keys¶
The default key is the positional arguments plus sorted keyword arguments (including self for methods), which must be hashable. Pass key= to choose what counts as "the same call", for example key=lambda path, **_: path.
Async and methods¶
Sync functions de-duplicate across threads. Async functions de-duplicate across tasks on the same event loop; the work runs in a shared task, so cancelling one waiting caller does not cancel it for the others.
Gotchas¶
State on methods
On a method, singleflight state belongs to the decorated function and is shared by every instance. The default key includes self, which separates instances; if you pass key=, include whatever must keep instances apart. See Limitations.