Uh oh!
There was an error while loading. Please reload this page.
feat: simplify async functionality to provider - #385
Conversation
df4fe5a to
a6b8325Comparebeeme1mr
commented
Nov 13, 2024
Hey @leohoare, thanks for the pr. I'm at KubeCon this week so reviews may be a bit delayed but I'll take a look as soon as possible. |
No rush @beeme1mr, enjoy KubeCon. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## main #385 +/- ##
==========================================
+ Coverage 97.54% 97.84% +0.29%
==========================================
Files 31 32 +1 Lines 1387 1622 +235 ==========================================
+ Hits 1353 1587 +234 - Misses 34 35 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
4f3e48d to
657ef96CompareUh oh!
There was an error while loading. Please reload this page.
| def get_metadata(self) -> Metadata: | ||
| return InMemoryMetadata() | ||
| def get_provider_hooks(self) -> typing.List[Hook]: | ||
| return [] |
There was a problem hiding this comment.
maybe I'm wrong, but I think these two should also be async 🤔
There was a problem hiding this comment.
Do you mean more generally or specifically to the InMemoryProvider?
For this PR, I was limiting the scope of adding async functionality to the resolving calls.
This ensures we have backwards compatibility with current hooks / metadata calls and providers would just need to implement the async resolve calls.
I do agree support async hooks and metadata retrieval should be looked at in the future though.
There was a problem hiding this comment.
Yeah, meant it more generally, because currently those would block the call, which is not really relevant for the in-memory variant, but would be for remote calls.
But totally fine to change this in the future.
0bfde37 to
17acf42Compare6740235 to
0d68182CompareSigned-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
Signed-off-by: leohoare <leo@insight.co>
leohoare
commented
Nov 19, 2024
The current approach wasn't very compatible with mypy, I've updated settings to ignore overrides. Can be overwritten to Also given we're not forking the typing protocol So I've ignored the error for that line. I'm not a heavy mypy user, let me know if there's a better or cleaner approach you'd prefer here. |
…ider Signed-off-by: Leo <37860104+leohoare@users.noreply.github.com>
beeme1mr
left a comment
There was a problem hiding this comment.
What's the expected behavior if a user registers an async provider but uses the synchronous client and vice versa? Could you please update the root readme examples for how to use this? If providers can't be used interchangeably, it may fragment the Python-related ecosystem in a confusing way.
| if not get_details_callable: | ||
| raise GeneralError(error_message="Unknown flag type") | ||
| resolution = await get_details_callable(*args) # type: ignore[misc] |
There was a problem hiding this comment.
Might be helpful to check whether get_details_callable is awaitable here in order to make sync provider compatible with async clients?
It might look like this, but I didn't test it
| resolution=awaitget_details_callable(*args) # type: ignore[misc] | |
| resolution=get_details_callable(*args) # type: ignore[misc] | |
| ifisawaitable(resolution): | |
| resolution=awaitresolution |
Typing can be a headache here, though.
There was a problem hiding this comment.
I actually think this has to be done. If not I think we would get type errors right?
@beeme1mr I think it's generally hard to perfectly support calling an async functions (from a provider) in a sync function (from a client) due to possibility of customized or already-started asyncio event loop, but calling sync function from async functions are much easier. I would still recommend implementing the async support because the performance is MUCH better in I/O intensive applications in comparison with threading based blocking I/O. However, we do need some error checking and documentation about incompatibility between sync clients and async providers. |
beeme1mr
commented
Jan 2, 2025
Hey @ChihweiLHBird, I'm definitely in favor of adding async support. I'm just familiar enough with Python to provide a solid recommendation here. My main concern is putting too much burden on the end user to understand what provider and hook are compatible with a particular client. @federicobond@gruebel@aepfli do you have any recommendations? |
beeme1mr
commented
Jan 2, 2025
Hey @leohoare, sorry for the delay and Happy New Year! This fell off my radar. Feel free to @ mention me if it happens again. |
@beeme1mr Thanks! And I just realized we may still make the async provider be compatible with sync client. This pattern below may be helpful to determine if the current execution is in an (Need more opinions on this pattern since I am not very familiar with it, I'm not sure if it's 100% reliable/secure) importasynciodefsome_client_functions():
try:
asyncio.ensure_future(asyncio.get_running_loop().create_task(some_async_io_task()))
exceptRuntimeError:
asyncio.run(some_async_io_task())However, an effort like this will make it extremely complicated, and it might be better and easier to just require asyncio clients for asyncio provider or sync clients for sync providers. |
Happy new year everyone! Apologies, this one has went on the back burner a bit. It's been quite a busy time for me personally. @ChihweiLHBird I do like the idea of falling back to the synchronous function in terms of usability. With other projects, I generally opt for a more explicit implementations and the async suffix on functions. In my eyes it leads to less confusion around the execution and could raise an exception when not implemented. flag=awaitget_boolean_details_async(...)It would require refactoring to use the async client though (rather than just replacing the client itself). It's mostly style preference though:
|
ChihweiLHBird
commented
Jan 7, 2025
@leohoare Most project I have seen implemented async stuff as you described, and I am also personally leaning to the style other projects used for our SDK here. |
aepfli
commented
Jan 7, 2025
Hey, sorry that i chime in here too. For me this seems like something, which we might should discuss even on a "global" open feature level. There are other languages, which also support async functionalities (like java with Futures) and maybe we should write a proper specification, on how we want to handle this. I think it could be problematic, if we do have mutliple different ways within our eco system to handle async. Hence it is worth to shortly discuss this on a bigger level. wdty? (@open-feature/technical-steering-committee fyi) |
beeme1mr
commented
Jan 7, 2025
I think it makes sense to include the async methods on the existing client. It should be possible for the async calls to be accessed via the async method (even if they're not truly async), meaning exiting providers would "just work." How would we want to support async-only providers? Keep in mind that the evaluation can't throw an error that bubbles up to the user. |
| flag_evaluation_options, | ||
| ) | ||
| async def evaluate_flag_details( # noqa: PLR0915 |
There was a problem hiding this comment.
there a lot of duplication of complex spec-compliance logic between the async and sync versions of evaluation_flag_details (~140 lines of code, with really the only difference being a single await when calling self._create_provider_evaluation).
Is there any way to share the implementations between the two?
lukas-reining
left a comment
There was a problem hiding this comment.
I am not a huge python expert, but I have a concern regarding the hard split between the sync and async world here.
If I am not seeing it wrong, a provider would either implement the sync or async world or both in this design. Is this correct?
If this is true: Do you see an option to have a sync provider and client while having an async client that wraps the sync client similar to:
classOpenFeatureClient:
def_evaluate_flag_details(
self,
flag_type: FlagType,
flag_key: str,
default_value: Any,
evaluation_context: Optional[EvaluationContext] =None,
flag_evaluation_options: Optional[FlagEvaluationOptions] =None,
) ->FlagEvaluationDetails[Any]:
# Dummy implementationreturnFlagEvaluationDetails()
defget_boolean_details(self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] =None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None) ->FlagEvaluationDetails[bool]:
returnself._evaluate_flag_details(FlagType.BOOLEAN, flag_key, default_value, evaluation_context, flag_evaluation_options)
asyncdef_async_wrap(self, func: Callable, *args, **kwargs) ->typing.Any:
loop=asyncio.get_event_loop()
returnawaitloop.run_in_executor(None, partial(func, *args, **kwargs))
asyncdefget_boolean_details_async(self, flag_key: str, default_value: bool, evaluation_context: typing.Optional[EvaluationContext] =None, flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None) ->FlagEvaluationDetails[bool]:
returnawaitself._async_wrap(
self.get_boolean_details, flag_key, default_value, evaluation_context, flag_evaluation_options
)The types and params might be a bit off, I just wanted to show the idea.
@open-feature/sdk-python-maintainers maybe you also have an opinion to this.
I would like to avoid "double" amount of work for implementers of providers if this is possible.
| if not get_details_callable: | ||
| raise GeneralError(error_message="Unknown flag type") | ||
| resolution = await get_details_callable(*args) # type: ignore[misc] |
There was a problem hiding this comment.
I actually think this has to be done. If not I think we would get type errors right?
lukas-reining
commented
Jan 7, 2025
This conforms to what I also wrote in my comment I just wrote @aepfli. |
lukas-reining
commented
Jan 7, 2025
I am totally with you with this @beeme1mr. The only thing I think I would question is |
leohoare
commented
Jan 7, 2025
Agree with all the above conversation, to summarise:
I'll have a think how to do this in python but I'm on the same page with the current code has way too much duplication. I'm not sure how this could be extended to support async hooks though. |
toddbaert
commented
Jan 8, 2025
hmmm... we might be forced to make all hooks async in this case, which is the case for server-side JS: https://github.com/open-feature/js-sdk/blob/main/packages/server/src/hooks/hook.ts That way they will work in either case... does that make sense? We are still pre-1.0 so now is the time if we need to make this sort of change. |
Fallback Sync FunctionalityTo achieve falling back on to the sync methods and not forcing async implementation. Adding default method to the Abstract provider is one approach. The downside being it wouldn't support only async implementations. It also wouldn't be an classAbstractProvider(FeatureProvider):
...
asyncdefresolve_boolean_details_async(
self,
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] =None,
) ->FlagResolutionDetails[bool]:
returnself.resolve_boolean_details(flag_key, default_value, evaluation_context)Async/Sync FunctionalityIt's hard to achieve what we're going for without some big drawbacks. Async wrappingIdea from @lukas-reining asyncdef_async_wrap(self, func: Callable, *args, **kwargs) ->typing.Any:
loop=asyncio.get_event_loop()
returnawaitloop.run_in_executor(None, partial(func, *args, **kwargs))
asyncdefget_boolean_details_async(self, flag_key: str, default_value: bool, evaluation_context: typing.Optional[EvaluationContext] =None, flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None) ->FlagEvaluationDetails[bool]:
returnawaitself._async_wrap(
self.get_boolean_details, flag_key, default_value, evaluation_context, flag_evaluation_options
)My main concerns:
Commonly merging async/syncA pattern used where entrypoints are async/sync (client), calling sync common code (client) and then forking logic for async/async (provider). defget_object_details_async(
self,
flag_key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] =None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None,
) ->FlagEvaluationDetails[typing.Union[dict, list]]:
returnself.evaluate_flag_details(
FlagType.OBJECT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
True, # flag for if async/sync
)
...
def_create_provider_evaluation(
ifasyncio.iscoroutinefunction(get_details_callable):
resolution=asyncio.run(get_details_callable(*args))
else:
resolution=get_details_callable(*args)This won't work because:
Converting downstream functions to all asyncdefget_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] =None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None,
) ->FlagEvaluationDetails[int]:
returnasyncio.run(
self.evaluate_flag_details(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
)
asyncdefget_integer_details_async(
self,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] =None,
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] =None,
) ->FlagEvaluationDetails[int]:
returnawaitself.evaluate_flag_details(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)
asyncdefevaluate_flag_details(
...Although this works as soon as we start running an event loop, we'll error out (APIs, async pytest). Abstracting common codeWhich leaves us the final option, abstracting common code and forking the calls, similar to what I've done but without duplicating the code base. I placed the draft code in this branch: #413, if we like the approach I'll force-push over this branch/PR. |
leohoare
commented
Jan 23, 2025
Closing in favour of #413. |
This PR
Adds the ability for open feature providers to use async methods
Note: This version simplifies the implementation and doesn't change any of the hook implementation.
Related Issues
#284
#383
Notes
Just confirming the approach / POC for how this would work.