-
-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Store.getsize #2426
base: main
Are you sure you want to change the base?
Added Store.getsize #2426
Conversation
# Note to implementers: this default implementation is very inefficient since | ||
# it requires reading the entire object. Many systems will have ways to get the | ||
# size of an object without reading it. | ||
value = await self.get(key, prototype=default_buffer_prototype()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason default_buffer_prototype()
isn't the default to get
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a good reason to avoid dynamic default arguments, e.g. function invocations (and I think there's a linting rule that complains about such things). But that doesn't change the fact that default_buffer_prototype()
is the default, which makes it annoying to specify. One idea that came up in the past was to statically associate a store with a buffer prototype, e.g. by making the store class generic, but this would reduce the flexibility of the store... although whether we realistically need the same store instance to return multiple different types of buffers is yet to be seen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be OK with defualt_buffer_prototype
being an optional argument (| None
, with a default of None, which means use default_buffer_prototype()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, some value like None
or the literal string "default"
would be perfect for this, at least in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two small bits of awkwardness:
- We'll now require Store implementors to read the documentation to understand what the value of
None / "default"
forprototype
means when they're implementingget
. I think that's fine, but merits documenting. - I might have missed it, but IIUC
zarr.core
is private, butzarr.core.buffer
isn't re-exported from anywhere else. I'll add it tozarr.buffer
unless anyone objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One idea that came up in the past was to statically associate a store with a buffer prototype
This thread came up in #1661, where I was struggling to understand the purpose of the prototype argument.
I think it would be nice to either statically associate stores with buffer prototypes or remove the prototypes altogether and let a store return a protocol instead of a specific class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks good, and I like the safe, slow default over returning -1
I might be confusing myself, but I think this implementation might not be what we want... I think what users want (like us in #2400) is the size of an Array in storage, not the size of a particular key. I guess we could do something like generate all the keys for a given array and then call So maybe we do need this, since the store is knows (or can figure out) what bytes are actually stored for a given array. But we also need a bit on top of it to bring it to the array level. |
In case you want to go this direction, this method is designed for exactly such a use case |
I'll throw an idea into the mix. We probably want two things:
Of course, |
Thanks. Looking at how Icechunk would implement Would you expect the size of metadata documents to show up in total for |
I've pushed an update that adds a |
Parameters | ||
---------- | ||
prefix : str | ||
The prefix of the directory to measure. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we offer implementers the following in documentation?:
This function will be called by zarr using a prefix
that is the path of a group, an array, or the root. Implementations can choose to do undefined behavior when that is not the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure... I was hoping we could somehow ensure that we don't call it with anything other than a group / array / root path, but users can directly use Store.getsize_prefix
and they can do whatever.
LMK if you want any more specific guidance on what to do (e.g. raise a ValueError
). I'm hesitant about trying to force required exceptions into an ABC / interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hesitant about trying to force required exceptions into an ABC / interface.
And now I'm noticing that I've done exactly that in getsize
, with requiring implementations to raise FileNotFoundError
if the key isn't found :)
""" | ||
keys = [x async for x in self.list_prefix(prefix)] | ||
sizes = await gather(*[self.getsize(key) for key in keys]) | ||
return sum(sizes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This materializes the full list of keys in memory, can we maintain the generator longer to avoid that?
Also, this has unlimited concurrency, for a potentially very large number of keys. It could easily create millions of async tasks. We should probably run in chunks limited by the value of the concurrency
setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See concurrent_map
for an example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This materializes the full list of keys in memory, can we maintain the generator longer to avoid that?
I don't immediately see how that's possible.
The best I'm coming up with is a fold-like function that asynchronously iterates through keys from list_prefix
and (asynchronously) calls self.getsize
to update the size. Sounds kinda complicated.
FWIW, it looks like concurrent_map
wants an iterable
of items:
> return await asyncio.gather(*[asyncio.ensure_future(run(item)) for item in items])
E TypeError: 'async_generator' object is not iterable```
In 7cbc5003eb4d397a7cf0f019bad85cd1c3d0927a I've hacked in some support for AsyncIterable there. I haven't had enough coffee to figure out what the flow of
return await asyncio.gather(*[asyncio.ensure_future(run(item)) async for item in items])
is. I'm a bit worried the async for item in items
is happening immediately, so we end up building that list of keys in memory anyway.
We should probably run in chunks limited by the value of the concurrency setting.
Fixed. We should probably replace all instances of asyncio.gather
with a concurrency-limited version. I'll make a separate issue for that.
Something like this interface
would be very useful for virtualizarr, as then we can easily and efficiently learn the byte range lengths of all objects in a store, in order to ingest existing zarr as virtual zarr. EDIT: xref zarr-developers/VirtualiZarr#262 (comment) |
Closes #2420
One difference from Zarr v2, its
getsize
seemed to return-1
if the concrete backend didn't provide agetsize
method. I think returning a "bad" integer like from a function that returns integers is dangerous. I've implemented a slow but correct default that just reads the object and callslen
on the bytes.[Description of PR]
TODO: