sailpoint-oss / python-sdk

MIT License
6 stars 3 forks source link

Feature Request: Retry-After Header support for rate limiting #10

Open rob-buskens-sp opened 6 months ago

rob-buskens-sp commented 6 months ago

Feature request

Incorporate support for Identity Security Cloud (ISC) API rate limits.

If ISC APIs return a 429 then try again after the number of seconds in the Retry-After header.

Rate Limits

There is a rate limit of 100 requests per access_token per 10 seconds for V3 API calls through the API gateway. If you exceed the rate limit, expect the following response from the API:

HTTP Status Code: 429 Too Many Requests

Headers:

Retry-After: {seconds to wait before rate limit resets}

Sample code coming?

I believe I've implemented this previously and will look for the code as an example. But don't wait for me! lol.

rob-buskens-sp commented 6 months ago

I found my code and it used https://urllib3.readthedocs.io/en/2.2.1/reference/urllib3.util.html which is what the python sdk appears to use for retries.

I had configured with a back off factor to wait longer on retries.

    retries = 3
    backoff_factor = 0.3
    retry_http_codes = (500, 502, 504, 429)
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist = retry_http_codes
    )

I didn't do any testing regarding it handling 429 responses and the retry-after header.

rob-buskens-sp commented 6 months ago

I did some testing and it appears that urllib3.Retry handles 429 and the Retry-After header with seconds. I didn't try dates.

rob-buskens-sp commented 6 months ago

express mock server

GET /api/users returns a 429 every 2nd invocation with varying waits

app.js

const app = express();
const port = 3000;

const routes = require('./routes');

app.use('/api', routes);

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

routes.js

const express = require('express');
const router = express.Router();

// Define routes
retry = false

// min and max included 
function randomIntFromInterval(min, max) { 
  return Math.floor(Math.random() * (max - min + 1) + min);
}

router.get('/users', (req, res) => {
  if (retry) {
    retryAfter = randomIntFromInterval(10, 60)
    console.log(`retry-after ${retryAfter}`)
    res.appendHeader('Retry-After', retryAfter).sendStatus(429);
  } else {
    console.log('200 all good')
    res.status(200).send({ status: 200, message: "all good"})
  }

  retry = !retry
});

router.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Details of user ${userId}`);
});

router.post('/users', (req, res) => {
  res.send('Create a new user');
});

router.put('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Update user ${userId}`);
});

router.delete('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`Delete user ${userId}`);
});

module.exports = router;

python test client with Retry

import urllib3
import logging
import time

from urllib3 import Retry

if __name__ == '__main__':
  logging.basicConfig(filename='retry.log',
                      filemode='a',
                      format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
                      datefmt='%H:%M:%S',
                      level=getattr(logging, "DEBUG"))

  requests_log = logging.getLogger("requests.packages.urllib3")
  requests_log.setLevel('DEBUG')
  requests_log.propagate = True

  timeout = urllib3.util.Timeout(connect=2.0, read=7.0)

  retries = Retry(
    total=5, status_forcelist = [ 502, 503, 504 ],
    respect_retry_after_header=True
  )

  http = urllib3.PoolManager(timeout=timeout, retries=retries)

  for x in range(100):

    resp = http.request("GET", "http://localhost:3000/api/users")

    print(x, resp.status)

Test

python client makes 100 calls, urllib3 logging shows every second call is a 429 and the retry occurring while the program output shows only the successful processing.

program output

0 200
1 200
2 200
3 200
4 200
5 200
6 200
7 200

log output

18:18:36,124 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:19:30,134 urllib3.connectionpool DEBUG Retry: /api/users
18:19:30,136 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:19:30,142 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:19:30,145 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:19:30,145 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:19:40,155 urllib3.connectionpool DEBUG Retry: /api/users
18:19:40,156 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:19:40,162 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:19:40,164 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:19:40,165 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:20:38,175 urllib3.connectionpool DEBUG Retry: /api/users
18:20:38,177 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:20:38,189 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0
18:20:38,191 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 429 0
18:20:38,192 urllib3.util.retry DEBUG Incremented Retry for (url='/api/users'): Retry(total=4, connect=None, read=None, redirect=None, status=None)
18:21:15,202 urllib3.connectionpool DEBUG Retry: /api/users
18:21:15,204 urllib3.connectionpool DEBUG Resetting dropped connection: localhost
18:21:15,213 urllib3.connectionpool DEBUG http://localhost:3000 "GET /api/users HTTP/1.1" 200 0

I suggest you'll want to do your own testing. On confirmation the feature request is to update the python-sdk doc: https://developer.sailpoint.com/docs/tools/sdk/python