Hey everyone :smile: hope you're having a nice wed...
# singer-tap-development
s
Hey everyone 😄 hope you're having a nice wednesday. I'm looking to implement client-side rate-limitting. I was thinking of using the ratelimiter library and place it on my tap - I was wondering on what function I should attach the decorator. build_prepared_requests ? or requests_decorator ? Thanks 😄
r
I am curious about this too, although that library hasn't been updated since 2017, so I don't know if it's a great idea to use it in a project now. I opened this issue last week, but I guess it could be expanded to generally better tools in the SDK to implement a rate limiting backoff strategy.
s
Glad I'm not the only one @Reuben (Matatika) 😄 I sadly need this to implemented yesterday, so I'll look into it using this library and let you know!
r
The maintainer of
ratelimit
recommended rush as an alternative (but also hasn't been updated in a while).
s
My issue is also where can I apply this method? I'm not sure on what I should be decorating 😅
@edgar_ramirez_mondragon you are usually my wizard 🧙 who knows the most about the singer-sdk - Any thoughts?
s
e
It's a requirement to use the decorator and just returns the bucket name and weight:
Limiter can be used as decorator, but you have to provide a
mapping
function that maps the wrapped function's arguments to
limiter.try_acquire
function arguments. The mapping function must return either a tuple of
(str, int)
or just a
str
https://github.com/vutran1710/PyrateLimiter#decorator
s
Sweet thanks! And has this affected your logging? I'm not sure how your
has_backoff
function works, is it based off the API you are using?
e
Yeah, the API response includes a
backoff
field when you hit a rate limit, so I use that to force a backoff in the client.
s
That'll teach me to just blindly copy paste things 😅 you gave me the big chunk, but I'll need to adapt your code to my use case. Does integrating this affect the current "back off" behavior?
(ex: exponential)
e
Yeah, my example replaces usage of the
backoff
library and does not perform exponential backoff
s
Is it possible to keep both?
e
Probably, with a bunch of nested function calls but I'd have to give it a try
s
I'm Going to have some fun exploring tonight then ;)
@edgar_ramirez_mondragon how did you get your bucket to take 3 paramets? It says it only take 2 from reading the docs: RateItem & rate
I thought if I got a 429, I could simply run a wait and try again, but it's not that easy right? Because the pyrate-limiter has no backoff mechanism, and I am replacing the "backoff" decorator with the "limitter" decorator, I am unable to do both at the same time. Therefore, I cannot both use the "limitter" decorator & the "backoff" decorator, and would need to *handle backoff in request.ge*t function call, wherever that is. Quite annoying 😅
Update - thought I had it with this:
Copy code
def request_decorator(self, func: Callable[..., Any]) -> Callable[..., Any]:
        """Decorate the request method of the stream.

        Args:
            func: The RESTStream._request method.

        Returns:
            Decorated method.
        """
        return limiter.as_decorator()(self._limiter_mapping)(
            super().request_decorator
        )(func)

    def _limiter_mapping(self, *_args: Any, **_kwargs: Any) -> tuple[str, int]:
        return self.tap_name, 1
But what ends up happening is simply that only the
super().request_decorator
is applied
e
But what ends up happening is simply that only the
super().request_decorator
is applied
Do you mean the pyrate_limiter exceptions are not being raised?
s
Yes, but I think I found it -> Put func in the wrong spot
Copy code
def request_decorator(self, func: Callable[..., Any]) -> Callable[..., Any]:
        """Decorate the request method of the stream.

        Args:
            func: The RESTStream._request method.

        Returns:
            Decorated method.
        """
        return limiter.as_decorator()(self._limiter_mapping)(
            super().request_decorator(func)
            # func
        )
Testing out, but now it should work 😄
Confirmed -> This works 😄
So for posterity, here is the full synopsis: If you wish to apply a logic which combines client-side rate limiting & backoff-error handling, you can do so by using the pyrate library, applying a delay and modifying the request_decorator function in your client. You are essentially adding a decorator on a decorator - this is known as decorator chaining. Here is an example of what I applied:
Copy code
def request_decorator(self, func: Callable[..., Any]) -> Callable[..., Any]:
        """Decorate the request method of the stream.

        Args:
            func: The RESTStream._request method.

        Returns:
            Decorated method.
        """
        return limiter.as_decorator()(self._limiter_mapping)(
            super().request_decorator(func)
            # func
        )

    def _limiter_mapping(self, *_args: Any, **_kwargs: Any) -> tuple[str, int]:
        return self.tap_name, 1
Note: It's important to note the order here - I want the backoff / error handling to be applied first so that it can handle any errors coming in from the API and only send clean info to the limiter
@edgar_ramirez_mondragon from your experience, how much could this query affect performance? I'm trying to see why, when applying the rate limiting, my speed went down by a factor of 4X. I'm wondering if it's 1. Pour setup 2. Compute heavy