Skip to content

Commit ea4e215

Browse files
committed
feat(adaptive-readahead): introducing new cache adaptive-readahead
1 parent 5e266e0 commit ea4e215

5 files changed

Lines changed: 822 additions & 0 deletions

File tree

fsspec/caching.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import collections
45
import functools
56
import logging
@@ -282,6 +283,95 @@ def _fetch(self, start: int | None, end: int | None) -> bytes:
282283
return part + self.cache[:l]
283284

284285

286+
class AdaptiveReadaheadCache(BaseCache):
287+
"""Adaptive readahead cache using the generic prefetch engine.
288+
289+
This cache remains API-compatible with existing fsspec cache wiring and
290+
falls back to classic ``ReadAheadCache`` when async prefetching is not
291+
available.
292+
"""
293+
294+
name = "adaptive_readahead"
295+
296+
def __init__(
297+
self,
298+
blocksize: int,
299+
fetcher: Fetcher,
300+
size: int,
301+
concurrency: int = 4,
302+
max_prefetch_size: int | None = None,
303+
) -> None:
304+
super().__init__(blocksize, fetcher, size)
305+
self._fallback = ReadAheadCache(blocksize, fetcher, size)
306+
self._prefetcher = None
307+
308+
async def _default_fetcher_async(
309+
start_offset: int,
310+
total_size: int,
311+
split_factor: int = 1,
312+
) -> bytes:
313+
del split_factor
314+
return await asyncio.to_thread(
315+
self.fetcher, start_offset, start_offset + total_size
316+
)
317+
318+
try:
319+
import fsspec.asyn
320+
321+
from .prefetch import BackgroundPrefetcher
322+
323+
self._prefetcher = BackgroundPrefetcher(
324+
fetcher=_default_fetcher_async,
325+
size=size,
326+
concurrency=concurrency,
327+
max_prefetch_size=max_prefetch_size,
328+
loop=fsspec.asyn.get_loop(),
329+
)
330+
except Exception as e:
331+
logger.debug(
332+
"AdaptiveReadaheadCache falling back to ReadAheadCache: %s",
333+
e,
334+
exc_info=True,
335+
)
336+
self._prefetcher = None
337+
338+
def _fetch(self, start: int | None, end: int | None) -> bytes:
339+
if self._prefetcher is None:
340+
out = self._fallback._fetch(start, end)
341+
self.hit_count = self._fallback.hit_count
342+
self.miss_count = self._fallback.miss_count
343+
self.total_requested_bytes = self._fallback.total_requested_bytes
344+
return out
345+
346+
out = self._prefetcher.fetch(start, end)
347+
self.miss_count += 1
348+
self.total_requested_bytes += len(out)
349+
return out
350+
351+
def close(self) -> None:
352+
if self._prefetcher is not None:
353+
self._prefetcher.close()
354+
self._prefetcher = None
355+
356+
def __getstate__(self) -> dict[str, Any]:
357+
# The prefetcher owns asyncio primitives that are not picklable.
358+
self.close()
359+
state = self.__dict__.copy()
360+
state["_prefetcher"] = None
361+
return state
362+
363+
def __setstate__(self, state: dict[str, Any]) -> None:
364+
self.__dict__.update(state)
365+
self._prefetcher = None
366+
367+
def __del__(self):
368+
try:
369+
self.close()
370+
except Exception:
371+
# Best-effort cleanup during GC.
372+
pass
373+
374+
285375
class FirstChunkCache(BaseCache):
286376
"""Caches the first block of a file only
287377
@@ -997,6 +1087,7 @@ def register_cache(cls: type[BaseCache], clobber: bool = False) -> None:
9971087
MMapCache,
9981088
BytesCache,
9991089
ReadAheadCache,
1090+
AdaptiveReadaheadCache,
10001091
BlockCache,
10011092
FirstChunkCache,
10021093
AllBytes,

0 commit comments

Comments
 (0)