Skip to content

Commit a652736

Browse files
antkrytcslzchen
andauthored
[ENG-11515] Async and cache metadata requests (#488)
* add cache to metadata calls for ZipStreamGenerator * implement cache as provider proxy; revert zip generator * fix circular import error * Apply suggestions from code review * Minor code style update + docstring Co-authored-by: Longze Chen <cslzchen@gmail.com> --------- Co-authored-by: Longze Chen <cslzchen@gmail.com>
1 parent 74c1a36 commit a652736

3 files changed

Lines changed: 59 additions & 7 deletions

File tree

waterbutler/core/cache.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import asyncio
2+
3+
4+
class CacheableMetadataProviderProxy:
5+
"""Wraps a provider so that folder ``metadata`` requests are cached and the
6+
metadata of any child folders is prefetched concurrently.
7+
8+
When a folder is listed, a metadata task is scheduled for each of its child
9+
folders without awaiting them. Those tasks run on the event loop while files
10+
are being downloaded, so the generator can issue all of its metadata requests
11+
up front instead of fetching folder listings strictly one-at-a-time.
12+
13+
Any attribute that is not overridden here (``download``, ``path_from_metadata``,
14+
etc.) is delegated to the wrapped provider.
15+
"""
16+
17+
def __init__(self, provider):
18+
self.provider = provider
19+
self.__cache = dict[str, asyncio.Task]()
20+
21+
def metadata_task(self, path, **kwargs):
22+
key = path.identifier or str(path)
23+
if key not in self.__cache:
24+
self.__cache[key] = asyncio.create_task(
25+
self.provider.metadata(path, **kwargs)
26+
)
27+
return self.__cache[key]
28+
29+
async def metadata(self, path, **kwargs):
30+
if path.is_file:
31+
return await self.provider.metadata(path, **kwargs)
32+
33+
items = await self.metadata_task(path, **kwargs)
34+
35+
for item in items:
36+
if item.is_folder:
37+
child = self.provider.path_from_metadata(path, item)
38+
self.metadata_task(child, **kwargs)
39+
40+
return items
41+
42+
def __getattr__(self, name):
43+
return getattr(self.provider, name)

waterbutler/core/provider.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from waterbutler import settings as wb_settings
1919
from waterbutler.core.metrics import MetricsRecord
2020
from waterbutler.core import metadata as wb_metadata
21+
from waterbutler.core.cache import CacheableMetadataProviderProxy
2122
from waterbutler.core.utils import ZipStreamGenerator
2223
from waterbutler.core.utils import RequestHandlerContext
2324

@@ -678,18 +679,17 @@ async def revalidate_path(self,
678679
"""
679680
return base.child(path, folder=folder)
680681

681-
async def zip(self, path: wb_path.WaterButlerPath, **kwargs) -> asyncio.StreamReader:
682+
async def zip(self, path: wb_path.WaterButlerPath, use_cache: bool = True, **kwargs) -> asyncio.StreamReader:
682683
"""Streams a Zip archive of the given folder
683684
684685
:param path: ( :class:`.WaterButlerPath` ) The folder to compress
686+
:param use_cache: ( `bool` ) Whether to prefetch metadata requests for nested folders during zip
685687
"""
688+
provider = CacheableMetadataProviderProxy(self) if use_cache else self
686689

687-
meta_data = await self.metadata(path, **kwargs) # type: ignore
688-
if path.is_file:
689-
meta_data = [meta_data] # type: ignore
690-
path = path.parent
691-
692-
return streams.ZipStreamReader(ZipStreamGenerator(self, path, *meta_data, **kwargs)) # type: ignore
690+
return streams.ZipStreamReader(
691+
await ZipStreamGenerator.create(provider, path, **kwargs)
692+
) # type: ignore
693693

694694
def shares_storage_root(self, other: 'BaseProvider') -> bool:
695695
"""Returns True if ``self`` and ``other`` both point to the same storage root. Used to

waterbutler/core/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def make_disposition(filename):
193193

194194

195195
class ZipStreamGenerator:
196+
196197
def __init__(self, provider, parent_path, *metadata_objs, **kwargs):
197198
self.provider = provider
198199
self.parent_path = parent_path
@@ -202,6 +203,14 @@ def __init__(self, provider, parent_path, *metadata_objs, **kwargs):
202203
]
203204
self.kwargs = kwargs
204205

206+
@classmethod
207+
async def create(cls, provider, path, **kwargs):
208+
meta_data = await provider.metadata(path, **kwargs)
209+
if path.is_file:
210+
meta_data = [meta_data]
211+
path = path.parent
212+
return cls(provider, path, *meta_data, **kwargs)
213+
205214
async def __aiter__(self):
206215
return self
207216

0 commit comments

Comments
 (0)