-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[ENH] add exponential backoff and jitter to embedding calls #1526
[ENH] add exponential backoff and jitter to embedding calls #1526
Conversation
Reviewer ChecklistPlease leverage this checklist to ensure your code review is thorough before approving Testing, Bugs, Errors, Logs, Documentation
System Compatibility
Quality
|
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.
@rancomp, have a look at my comments.
@@ -31,6 +33,19 @@ | |||
logger = logging.getLogger(__name__) | |||
|
|||
|
|||
def _retry_call(call): |
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.
A few comments on this:
- Does it make sense to move this to a separate util module
- Does it make sense to have
_
for a decorator? Seems odd - This decorator seems to erase type info.
- Does it make sense to add configuration options e.g. how many retries, how much to wait, ignored exceptions?
- The
@retry
decorator seems to throw RetryException. Does it make sense to raise the original exception as it will be better DX
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.
Thanks @tazarov!
A few comments on this:
- Does it make sense to move this to a separate util module
Yes that makes sense. But then again I don't see it used elsewhere at the moment.
- Does it make sense to have
_
for a decorator? Seems odd
I agree with you. It was just a leftover from my first commit where I had it as a private function within the class
- This decorator seems to erase type info.
Is it because I'm using *args
? This can be fixed by explicitly naming the arguments. I should also annotate the outputs Embeddings
.
- Does it make sense to add configuration options e.g. how many retries, how much to wait, ignored exceptions?
Yes that makes sense. My idea was to have each EmbeddingFunction instance carry its own arguments for Tenacity.retry
through self.wait
, which is what I wanted to achieve with the super().__init__()
. Another thing I can do is avoid the super()
all together and just let the call_wrapper_factory
check whether self
has attribute wait
. If not, set the decorator to be the default wait_exponential_jitter()
.
- The
@retry
decorator seems to throw RetryException. Does it make sense to raise the original exception as it will be better DX
Important catch here. I'll see how tenacity suggest raise the original exception.
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.
AFAIK there's no way to implement a decorator like this in python <= 3.10
without destroying type info. Our auth and telemetry decorators destroy type info and it's something I would like to fix.
The fact that we can't use a decorator and preserve type info makes me think we shouldn't do it. Instead, we could (in order of my preference off the top of my head):
- Use a contextmanager
- Just use
tenacity
directly wherever we call out to embedding providers. - Have an explicit function
try_with_retries
or something, which takes retry parameters and the relevant method call.
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.
Hey @beggers . I pushed a small update addressing @tazarov's remarks.
Regarding your points:
AFAIK there's no way to implement a decorator like this in python
<= 3.10
without destroying type info. Our auth and telemetry decorators destroy type info and it's something I would like to fix.The fact that we can't use a decorator and preserve type info makes me think we shouldn't do it. Instead, we could (in order of my preference off the top of my head):
- Use a contextmanager
I'm not familiar with contextlib but I'm looking into it now. I want to ask, IIUC the problem with type-info exists in other places in the repo. Should we push for a solution that solves all of these together?
- Just use
tenacity
directly wherever we call out to embedding providers.
- Have an explicit function
try_with_retries
or something, which takes retry parameters and the relevant method call.
I can do both. Let me think about these.
@rancomp I see this PR is still a draft -- tag me when it's ready for a review and I'll take a look |
Hey @beggers .I'm finding it challenging to create a straightforward backend along with a user-friendly API for this task. My initial attempt involved the use of the class attributes to edit the retry parameters, but that doesn't feel right. Let's start with a simple solution which is your 3rd suggestion. |
Sorry for my lateness here. I like the approach of having One other option for us to consider: We could give each |
hey @beggers NP. Compare this with the newly added I'm leaning towards the |
…-exp-backoff-and-jitter-with-tenacity
…-exp-backoff-and-jitter-with-tenacity
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.
hey @rancomp , sorry I dropped this. Looks good! If you fix the merge conflicts I'll run the precommit hooks and get this merged.
alright @beggers np! PS, I got a weird message flake error when merging main into my branch:
Is it worth adding |
merged main (conflicts), but black hook caught some stuff from other modules and corrected it. |
@@ -194,6 +195,9 @@ def __call__(self: EmbeddingFunction[D], input: D) -> Embeddings: | |||
|
|||
setattr(cls, "__call__", __call__) | |||
|
|||
def embed_with_retries(self, input: D, **retry_kwargs: Dict) -> Embeddings: | |||
return retry(**retry_kwargs)(self.__call__)(input) |
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.
Now that I think about this, this retry will work even if the errors from OPENAI are non transient correct? In cases where let say user max send limit for their Openai is reached no amount of retry is going to fix it until they update their spend limit. Do we want to retry for non transient errors? I guess consumers of chromaDb should be handling this no 🤔
I am a user of chromaDB and what I have seen usually is Open AI waiting 600 secs and returning Timeouts. And literally so many folks complain about this error on their forum - https://community.openai.com/t/frequent-api-timeout-errors-recently/106903
We should also have a way to not wait 600 seconds and allow consumers to configure this. Ideally openAI should have given us a configuration option but there does not seem to be one.
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 haven't used tenacity
directly before (others on the team have and it's what we use elsewhere in the codebase), but it looks like it allows you to set a timeout: https://tenacity.readthedocs.io/en/latest/#stopping . I didn't see anything in my skimming about retrying only certain error codes though I'm sure it's possible.
@rancomp designed this implementation so Chromadb users have full control over the tenacity
retry logic so it should be plug-and-play to get this working.
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.
@rancomp sorry for the slog here. CI is currently broken. I'll merge it into this branch and re-run tests once we've fixed it -- no action required from you and we'll get this over the finish line.
@@ -194,6 +195,9 @@ def __call__(self: EmbeddingFunction[D], input: D) -> Embeddings: | |||
|
|||
setattr(cls, "__call__", __call__) | |||
|
|||
def embed_with_retries(self, input: D, **retry_kwargs: Dict) -> Embeddings: | |||
return retry(**retry_kwargs)(self.__call__)(input) |
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 haven't used tenacity
directly before (others on the team have and it's what we use elsewhere in the codebase), but it looks like it allows you to set a timeout: https://tenacity.readthedocs.io/en/latest/#stopping . I didn't see anything in my skimming about retrying only certain error codes though I'm sure it's possible.
@rancomp designed this implementation so Chromadb users have full control over the tenacity
retry logic so it should be plug-and-play to get this working.
This is a WIP, closes #1524
Summarize the changes made by this PR.
tenacity
to add exponential backoff and jittertenacity
's APITest plan
How are these changes tested?
pytest
for python,yarn test
for jsDocumentation Changes
None