feat(once): Add async LazyCell implementation - #168
Conversation
|
Thanks for your contribution @mhambre! Feel free to ping me when this PR is ready for review. |
|
@tisonkun Before I put a bow on this, what are your thoughts on bullet #1? Supporting resumability would require some additional machinery, but I can implement it if we think that’s a semantic guarantee LazyLock should provide. I went with the simpler approach for now since I wasn’t convinced the additional complexity was justified without that requirement. |
|
Thanks for raising this. My current inclination is that Before settling the API, though, I think we'd better validate it against the motivating OpenDAL use case. Could you temporarily point OpenDAL at this PR’s commit by using a pinned The key concern is when the initializer inputs become available. With A small compiling prototype, ideally with the relevant test, would tell us whether a zero-argument stored initializer actually fits OpenDAL or whether the API/context ownership needs adjustment. If the use case works, I would prefer the BTW, I'd prefer to call the primitive |
c27f769 to
683cc8e
Compare
|
@tisonkun this should be good for a review. Went through and did some cleanup and feature changes aligned with your request. I tried to provide as much detail as possible in the PR description about design choices and API decisions. Here is the example PR in OpenDAL as requested apache/opendal#8133. If you need any clarification to help aid in your review feel free to ask. |
Adds support for an asynchronous
LazyCellinspired by async-lazy and the APIs provided bystd::sync::LazyLockandstd::cell::LazyCell.The main addition to this is fallible initialization through
LazyCell::try_forceand being runtime agnostic (async-lazy is married to Tokio through sync primitives) by using asyncband'sMutexandOnceCell.Some design choices:
try_methods.Mutex. The state stores at most one pinned attempt, which remains available for the next caller after cancellation. AnErrremoves the completed attempt but retains theFnMutinitializer, allowing the next queued caller to start its own attempt. A successful value is stored in theOnceCell.Send+'staticfuture.Sendis required because resuming may happen on a different thread, while'staticis required because the stored future may outlive the caller that started it.FnMut() -> Future<Output = Result<T, E>>overAsyncFnOncebecause returning an error must leave the initializer available for another attempt. This design choice was made particularly becauseasyncprograms open the door to IO-based failures outside of the programmer's control, and we want to allow users to handle those failures gracefully. A good example is the OpenDAL implementation, where the HuggingFace API going down temporarily should not permanently prevent canonicalization.std::sync::LazyLockandstd::cell::LazyCell,getmethods are introspective, whileforcemethods actively perform initialization. We also maintain the use of associated functions over methods.LazyCellhappen through associated functions._withfunctions that take a single argument. This can be a tuple or struct to allow for multiple arguments. Because the returned future is'static, the initializer can borrow a large argument, clone only the fields it needs, and move those fields into the future:This implementation is in reference to #160. An example of it used in practice inside of OpenDAL's HuggingFace service can be found here.