Cómo vivir con límites de API externos en el número de solicitudes

Muchos servicios brindan la capacidad de interactuar con ellos no solo para usuarios comunes a través de interfaces gráficas pulidas y optimizadas, sino también para desarrolladores externos desde sus programas a través de la API. Al mismo tiempo, es importante que los servicios controlen la carga en su infraestructura. En la situación con usuarios comunes, la mayoría de los problemas de carga no surgirán debido al control del código de la aplicación que envía solicitudes al servicio por parte de los desarrolladores del servicio (usuarios que intentan hacer algo en la aplicación fuera del marco de las interfaces propuestas por los desarrolladores y las capacidades documentadas, estamos en este artículo no considerado). En el caso de los desarrolladores externos, el alcance para crear una carga en el servicio está limitado solo por la imaginación de estos desarrolladores externos. Para limitar un poco este espacio,La práctica de introducir restricciones en el número de solicitudes por unidad de tiempo a la API del servicio se ha generalizado. 

, , API , , «» API.

, Data Platfrom ManyChat. , , , Intercom, In-App -. , Intercom (, , ..). Intercom - -, . , , ( ), , -. , ML- . , Intercom.

: , , API 1000 . , Intercom, .

, API , . «» «» -, .

- , « » API, API, API. , , API .

« »

, ManyChat Redis — . « » - , API . , API, «», , - . , , «» , - Intercom, , «» .

Redis, List .

, API, consumer API. rate-limit, , , .

— «» - ( BackendQueue), «» (AnalyticsQueue). , , consumer, , .

(JSON):

{
    "method_name": "users_update", //  ,   
    "parameters": {"user_id": 123} // ,       
}

MVP consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    
    ...
    
    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        while (true) {
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }
}

, , — .

:

Backend (PHP):

...
$payload = [
    'method_name' => 'users_update',
    'parameters' => ['user_id' => 123, 'registration_date' => '2020-10-01'],
];
$this->getRedis()->rawCommand('RPUSH', 'BackendQueue', json_encode($payload));
...

(Python):

...
payload = {
    'method_name': 'users_update',
    'parameters': {'user_id': 123, 'advanced_metric': 42},
}
redis_client.rpush('AnalyticsQueue', json.dumps(payload))
...

— , Intercom, . — - , API «» , rate-limit, customer'a rate-limit', , - . Redis ( ) consumer'. , , consumer', , . , , , , .

, , consumer' , . consumer' , API .

consumer'a (PHP)
class APICaller
{
    private const RETRIES_LIMIT = 5;
    private const RATE_LIMIT_TIMEFRAME = 10;
    private const INTERCOM_RATE_LIMIT = 150;
    private const INTERCOM_API_WORKERS = 5;

    ...

    public function callMethod(array $payload): void
    {
        switch ($payload['method_name']) {
            case 'users_update':
                $this->getIntercomAPI()->users->update($payload['parameters']);
                break;
            default:
                throw new \RuntimeException('Unknown method in API call');
        }
    }

    public function actionProcessQueue(): void
    {
        $currentTimeframe = $this->getCurrentTimeframe();
        $currentRequestCount = 0;
        
        while (true) {
            if ($currentTimeframe !== $this->getCurrentTimeframe()) {
                $currentTimeframe = $this->getCurrentTimeframe();
                $currentRequestCount = 0;
            } elseif ($currentRequestCount > $this->getProcessRateLimit()) {
                usleep(100 * 1000);
                continue;
            }
            
            $payload = $this->getRedis()->rawCommand('LPOP', 'BackendQueue');
            if ($payload === null) {
                $payload = $this->getRedis()->rawCommand('LPOP', 'AnalyticsQueue');
            }

            if ($payload) {
                $retries = 0;
                $processed = false;
                while ($processed === false && $retries < self::RETRIES_LIMIT)
                {
                    try {
                        $this->callMethod(json_decode($payload));
                        $processed = true;
                    } catch (IntercomRateLimitException $e) {
                        $retries++;
                        sleep(self::RATE_LIMIT_TIMEFRAME);
                    }
                }
            } else {
                sleep(1);
            }
        }
    }

    private function getProcessRateLimit(): int
    {
        return (int) floor(self::INTERCOM_RATE_LIMIT / self::INTERCOM_API_WORKERS);
    }

    private function getCurrentTimeframe(): int
    {
        return (int) ceil(time() / self::RATE_LIMIT_TIMEFRAME);
    }
}

API

- API, . API . — . , , callback'e, consumer' . callback', , .

, , , , .

, , //?

, API , rate-limit, . , . , , , , , .

, ,   .

API, , , API , API .

, - . , API .




All Articles