bunq / sdk_php

PHP SDK for bunq API
MIT License
83 stars 54 forks source link

Add middleware/handler to throttle requests to conform to API rate limit #109

Open holtkamp opened 6 years ago

holtkamp commented 6 years ago

The API employs a rate limit, which is understandable.

next to the (outdated?) documentation the following seems to apply:

A way to deal with this is:

This is a rather "reactive" approach and unintelligent (the minimum number of seconds to sleep seems not available in the exception?). It would be nice to have a middleware in the API client that keeps track of the number of requests submitted to each endpoint and applies throttling when required.

Since the client performs some requests when connecting. Such a throttling mechanism can not solely be implemented in userland code. A throttling approach also prevents sleeping during a test 😨 .

OGKevin commented 6 years ago

@holtkamp thanks for this suggestion. Ill tackle this in 0.13.0.

holtkamp commented 6 years ago

@OGKevin ok, nice, in case you need inspiration, implemented something which currently "does the job"...

OGKevin commented 6 years ago

@holtkamp What do you mean with implemented something which currently "does the job". The way the ApiClient Is build it would not be possible to remake a call after a 429 has been caught. The only way to achieve this is to add a middleware before the request is sent and keep the count of requests per seconds set in BunqContext.

Then calculate if this request may trigger a 429 or not. And based on that sleep or not. But of course im open to other suggestions.

holtkamp commented 6 years ago

The only way to achieve this is to add a middleware before the request is sent and keep the count of requests per seconds set in BunqContext.

Indeed, that approach is what I implemented. Will check coming week if I can share some details

holtkamp commented 6 years ago

Currently using this approach: https://gist.github.com/holtkamp/bae00c495f80eace6aa49d12b46f5bca, maybe not very elegant, but does the job

holtkamp commented 6 years ago

Apparently what we are looking for is a "LeakyBucket" implementation. Look at this PSR-7 implementation of it: https://github.com/robwittman/leaky-bucket-rate-limiter

holtkamp commented 6 years ago

@OGKevin any update / progress on this aspect?

OGKevin commented 6 years ago

No not really 😭. Im packed with internal projects and therefore didn't get time to work on github projects. PR's are highly appreciated tho. Next week ill be on holiday so i might do some hobby coding and work on github then.

Some one made a PR for c# https://github.com/bunq/sdk_csharp/pull/106#pullrequestreview-133114225 which looks good.

holtkamp commented 5 years ago

Happy anniversary for this issue! πŸŽ‚

While Bunq is pushing a lot of "crazy awesome nice amazing" new features in well marketed "product updates", it seems some basic functionality like request throttling of the official client of a throttled API is not a priority.

@OGKevin are sufficient resources assigned to API (client) development?

OGKevin commented 5 years ago

While Bunq is pushing a lot of "crazy awesome nice amazing" new features in well marketed "product updates", it seems some basic functionality like request throttling of the official client of a throttled API is not a priority.

thats exactly the case :P and has always been. Sorry i could not have had a proper look at this.

I no longer work at bunq, hence i've been removed as maintainer. @kojoru took over, but it looks like he is running in the same issue as i was πŸ˜‰. Im happy to dive into this and do open source contribution tho. ill try to have a look this weekend before i go on holiday. If not afterwards.

I haven't used this SDK anymore, i've switched to golang.

holtkamp commented 5 years ago

@OGKevin thanks for the response

@kojoru maybe you can convince decision makers to assign some of the "marketing budget" (for example, this: https://twitter.com/bunq/status/1090923033223745537) to sponsoring OSS contributions. There are plenty skilled developers who can help improve the API client once they get some support to be able to spend time on this.

OGKevin commented 5 years ago

So i took a look at this, and it seems that the current way the request and response handlers are created: https://github.com/bunq/sdk_php/tree/e9511e1c158a2c8d768d167bc05a66d7d88ea5d5/src/Http/Handler

The response handlers do not have the request πŸ€” So, the first task would be is to rework the response handlers to so that they get the request as well. Once this is done, then we can create a new response handler that checks for 429, and if that is the case then returns a recursive call to the client again to execute that request.

I'll work on a pr to give the response handlers the request.

OGKevin commented 5 years ago

I've created a PR. the only thing that we need to figure out now is how we're going to test an actual request. I hate the fact that the SDK is making real requests which makes the test suite super slow :( never got the chance to implement a nice request mocker.

Nevertheless, how can we easily trigger a 429 that goes through the api client πŸ€” https://github.com/bunq/sdk_php/blob/a1c1ba133aff7d97126003c65511b1873cc1d183/tests/Http/ResponseHandlerRateLimitTest.php#L60-L66

OGKevin commented 5 years ago

just to clarify my retry approach, as this SDK is build based on static methods and singletons. It makes no sense to keep a global mapping to throttle the requests instead of retrying on failures. If we had a long living api client, then this client could have kept a map of uri's and throttle the requests accordingly.

holtkamp commented 5 years ago

If we had a long living api client, then this client could have kept a map of uri's and throttle the requests accordingly.

Well, in my case the client gets initialized on a daily basis to perform a batch of operations. So this client is "long living".

I think the "retry approach" to deal with 429 errors is fundamental to have, but can be extended with the "request throttling" approach to (try to) prevent even receiving those 429 errors...

holtkamp commented 5 years ago

the only thing that we need to figure out now is how we're going to test an actual request. I hate the fact that the SDK is making real requests which makes the test suite super slow :( never got the chance to implement a nice request mocker.

Maybe we can have a look at https://github.com/picqer/moneybird-php-client/tree/master/tests for inspiration. I contributed to that library a few times and if I remember correct the testing of the connection with a mocked connection was quite decent...

Also see:

OGKevin commented 5 years ago

Well, in my case the client gets initialized on a daily basis to perform a batch of operations. So this client is "long living".

Thats not what i meant, what i meant was that for each Api call, a new client is being made. E.g. https://github.com/bunq/sdk_php/blob/e9511e1c158a2c8d768d167bc05a66d7d88ea5d5/src/Model/Generated/Endpoint/MonetaryAccountBank.php#L409 and

https://github.com/bunq/sdk_php/blob/e9511e1c158a2c8d768d167bc05a66d7d88ea5d5/src/Model/Generated/Endpoint/Payment.php#L355

This makes the client super short lived and therefore it can not nicely keep track of all the previous requests it made and therefore nicely throttle if it suspects a 429 to happen.

Maybe we can have a look at https://github.com/picqer/moneybird-php-client/tree/master/tests for inspiration.

I see that they indeed use mocks and not make actual requests which is nice. But, in our case, mocking is kind of hard. As bunq's approach is to make everything static. And the fact that you have this https://github.com/bunq/sdk_php/blob/e9511e1c158a2c8d768d167bc05a66d7d88ea5d5/src/Model/Generated/Endpoint/MonetaryAccountBank.php#L443-L457

Instead of the class having a client as field, makes it even harder to have a test double client that fakes the requests. What i normally try to do is use interfaces and make 2 implementations. One for test and one for prod, as you could see here https://github.com/bunq/sdk_php/blob/4c13a410ed184c2ccab929fc929d7fb9a0d2ff2c/src/Http/RequestRetryer.php#L12-L18

However, IMO there should not be an interface for this, however there should be an interface that defines the https://github.com/bunq/sdk_php/blob/e9511e1c158a2c8d768d167bc05a66d7d88ea5d5/src/Http/ApiClient.php#L212-L218 method. This way, we can have 1 "requester" for production that makes actual requests, and 1 "requester" for test that fakes requests. This makes it so that you don't even need to use a mock, just an implementation for testing.

OGKevin commented 5 years ago

I guess we can drop all hopes of getting my PR looked at and merged πŸ€·β€β™‚ Im really sorry @holtkamp that I could not have done this while I still was a maintainerπŸ˜”.

holtkamp commented 4 years ago

Additional inspiration for the Bunq team:

https://github.com/spatie/guzzle-rate-limiter-middleware

bosd commented 3 years ago

Seems like another anniversary :birthday: for this issue. Anyone found an alternative solution or user implementation?