-
Notifications
You must be signed in to change notification settings - Fork 38
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
feat(retry) add optional randomness min/max #50
base: master
Are you sure you want to change the base?
Conversation
To help when accessing heavily loaded servers
A few thoughts:
|
Yes, I think handling network/server failures should be outside the core dicomweb logic. Being able to pass in a Using something like envoy could be a good solution. |
@pieper I generally don't have any major concerns to expose additional retry parameters. The only thing we should keep in mind that we may want to change the implementation at some point and use another library under the hood for implementing the retry logic. |
@hackermd, thoughts on allowing the user to pass an optional sequence of decorators which would wrap all of the http calls? would decouple the retry logic, streamline the addition of new functionality, and could be done in a backwards-compatible fashion. Note: Depending on the interface, this could possibly be used to layer on custom authn as well. |
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 @pieper. Some minor comments related to naming, type annotations and docstrings.
@@ -141,6 +141,8 @@ def set_http_retry_params( | |||
retry: bool = True, | |||
max_attempts: int = 5, | |||
wait_exponential_multiplier: int = 1000, | |||
wait_random_min: int = None, |
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.
wait_random_min: int = None, | |
wait_random_min: Optional[int] = None, |
@@ -141,6 +141,8 @@ def set_http_retry_params( | |||
retry: bool = True, | |||
max_attempts: int = 5, | |||
wait_exponential_multiplier: int = 1000, | |||
wait_random_min: int = None, | |||
wait_random_max: int = None, |
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.
wait_random_max: int = None, | |
wait_random_max: Optional[int] = None, |
@@ -164,6 +167,10 @@ def set_http_retry_params( | |||
the maximum number of request attempts. | |||
wait_exponential_multiplier: float, optional | |||
exponential multiplier applied to delay between attempts in ms. | |||
wait_random_min: float, optional |
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.
wait_random_min: float, optional | |
wait_random_min: int, optional |
@@ -164,6 +167,10 @@ def set_http_retry_params( | |||
the maximum number of request attempts. | |||
wait_exponential_multiplier: float, optional | |||
exponential multiplier applied to delay between attempts in ms. | |||
wait_random_min: float, optional | |||
minimum amount of jitter in ms. | |||
wait_random_max: float, optional |
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.
wait_random_max: float, optional | |
wait_random_max: int, optional |
@@ -141,6 +141,8 @@ def set_http_retry_params( | |||
retry: bool = True, | |||
max_attempts: int = 5, | |||
wait_exponential_multiplier: int = 1000, | |||
wait_random_min: int = None, |
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 remove the random
from the parameter names and just use wait_min
and wait_max
rather than wait_random_min
and wait_random_max
, respectively?
@hackermd I like the suggestion from @ntenenz that the retry logic should be outside of the library itself, or to avoid breaking changes we keep your current minimal implementation without exposing new options (i.e. drop this PR). For my use case I would have wanted to use the logging feature of |
FWIW @pieper, an easy way to ensure backwards-compatibility would be to:
|
Let me think about this. It seems like an elegant and more general approach. Another possibility for how this could be achieved without making any changes to the library code is by passing a |
@pieper did you try the |
No, I didn't see the event hooks. Not sure that would have been exactly on point for my needs without more work though since I wanted to just know what requests were being retried and why, retry seemed like it would have been a one-liner. |
If it's primarily about getting logging output, what about passing the existing logging object to the retry call? Getting the retry warning messages shouldn't do any harm and would probably be appreciated by a lot of users. In the meantime, we can further explore @ntenenz's proposal. |
@ntenenz would you kindly further elaborate on what you are proposing. Are you suggesting to add a parameter to the constructor to pass a higher-order function that can wrap request HTTP method calls? So the caller would for example pass an "instance" of |
Currently,
I'm proposing that these methods retain their current signatures and that the user be allowed to supply, via |
@ntenenz Thanks for the elaboration.
I think that the decorator approach is elegant and would give users a lot of control. However, it would also expose low level implementation details and would be difficult to document (just try to document the signature of the callable with type annotations). Are there any other use cases beyond retrying where it would be useful to pass custom decorators for these functions? |
Logging and retries are the two obvious ones. Related to logging could be auditing and gathering performance or usage statistics. Perhaps also injecting headers or otherwise manipulating the request or response could be useful. |
As @pieper stated, header manipulation (auth, tracing, etc), logging, retry ... the list goes on. Regarding the exposing of internal details, I'd argue url, body, and headers are fairly generic and could serve as an API between the client and user without binding yourself to a particular implementation. |
Couldn't most of the features in this list already be supported by implementing a class derived from I don't want to get into a discussion of object-orientated versus functional programming paradigms, but am wondering whether it will ultimately be beneficial to support several mechanisms to manipulate requests/responses that could potentially interfere with each other. I am not categorically against the decorator approach, but still have doubts whether we would really need it to achieve @pieper's goal. I like the fact that the retry logic is currently abstracted from the user, which seems to cover the needs of most users. If we decide to allow users to pass decorators, there are two things we would need to sort out:
|
Honestly, to me this is not at all about functional vs object-oriented programming, but rather, providing flexibility to the user without the need to create derived classes as well as consolidation of logic. For the instances you just described, the user would need to
This means the user must be aware of (and is bound to) the underlying implementation details. If you ever wanted to move from requests to some other library, user code would break. Additionally, logic for this is distributed between the Session, other API calls, etc. Alternatively, if you take the decorator approach, you're extending the API surface and providing a contract to the user. They need not know/care HOW the request is issued, only that your API is remaining static. Furthermore, request modification logic is consolidated in a single location, providing a single source of truth. Imagine a situation where I wanted to employ custom retry logic, a different auth scheme, add some headers, do some custom logging, deploy distributed tracing, etc -- all customizable via a config file. I could:
Simply put, composition >>> inheritance here. This could be achieve by inserting callable objects or decorators, but the result is largely identical. To answer your questions at the bottom,
CallableType = Callable[[URLType, BodyType, HeaderType], ResponseType]
def my_decorator(f: CallableType) -> CallableType:
def func(url: URLType, body: BodyType, headers: HeaderType) -> ResponseType:
# do something
out = f(url, body, headers)
# do something else
return out
return func
|
The decorator approach still relies on the requests API. The decorate will have to rely on the wrapped function to return an instance of If the underlying implementation changes, e.g., if we would move away from requests, the decorator would have the same problem. |
How would that really be different with a decorator? The user would have to implement a decorator that wraps I don't want to argue about the pros and cons of either approach (inheritance versus composition), but discuss whether we should support both. I agree with you that the latter would have advantages. However, we already allow the user to pass an instance of |
If we were to decorate the function call as defined in my sample above, decorators could be composed arbitrarily by placing them in a list and combining them at runtime. If we were to create a derived class for each piece of additional functionality, functionality could not be combined unless a) the combination was predefined (less flexible) or b) the classes were created dynamically at runtime (more complex).
Indeed. But you'd have already provided an abstraction over the request itself. Placing an abstraction over the response would presumably be far easier, especially given that At the end of the day, you're the owner/maintainer of this library. The decision is fundamentally up to you. I'm just trying to provide feedback. |
Agreed, that would be nice (similar to the torchvision.transforms.functional API). I would suggest implementing the approach and play with it. That will probably help us with the remaining API design questions. @pieper do you want to give it a try for your use case? |
Yes, now that we have a good plan I can try that out next time I do a dive into that use case. Now that I know what the core issue was (hitting the 5000 requests/minute quota) I don't need the logging function as much as I did before. But a logging decorator or other ways of manipulating the requests would have made debugging much simpler so it's still worth adding. |
@pieper did you try the Moving forward, I think we could introduce |
I believe there are two axes worth distinguishing and worthy of discussion here.
Personally, I lean (slightly) towards the decorator approach for the former, but see the merits of both. For the latter, we may be able to achieve both by accepting either a list or a dict whereas a list would apply all actions to all requests and the dict would map request type -> action. Thoughts? |
I did not - now that I know what the issue is I no longer need to log/debug. But I can foresee similar issues coming up in the future, so it'll be good to have a plan. Regarding the axes that @ntenenz raises, I think it's better to keep the logic in the hook/decorator and not in the dicomweb-client or requests library. That is, I may want to only log certain request types but only when they have a particular url parameter or all request types that hit a particular studyUID. So I think it's enough to have a list of hooks or cascade of decorators. |
Quality Gate failedFailed conditions See analysis details on SonarQube Cloud Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE |
To help when accessing heavily loaded servers. Google's dicom stores for example seem very prone to overloading, in which case they return a 429 code.
This exposes a few more flags from the core library. It would be nicer to somehow expose the native interface to avoid this kind of API duplication.
Even better might be a way to support different mechanisms. The
retry
package for instance supports logging, which would be helpful when picking parameters