Skip to content

feat!: introduce rate limiting #42

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

Merged
merged 7 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ Part of the <a href="https://github.com/twilio-labs/serverless-toolkit">Serverle
<img alt="npm (scoped)" src="https://img.shields.io/npm/v/@twilio-labs/serverless-api.svg?style=flat-square"> <img alt="npm" src="https://img.shields.io/npm/dt/@twilio-labs/serverless-api.svg?style=flat-square"> <img alt="GitHub" src="https://img.shields.io/github/license/twilio-labs/serverless-api.svg?style=flat-square"> <a href="#contributors"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square" /></a> <a href="https://github.com/twilio-labs/.github/blob/master/CODE_OF_CONDUCT.md"><img alt="Code of Conduct" src="https://img.shields.io/badge/%F0%9F%92%96-Code%20of%20Conduct-blueviolet.svg?style=flat-square"></a> <a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" /></a> </<a>
<hr>

- [Installation](#installation)
- [Example](#example)
- [API](#api)
- [`client.activateBuild(activateConfig: ActivateConfig): Promise<ActivateResult>`](#clientactivatebuildactivateconfig-activateconfig-promiseactivateresult)
- [`client.deployLocalProject(deployConfig: DeployLocalProjectConfig): Promise<DeployResult>`](#clientdeploylocalprojectdeployconfig-deploylocalprojectconfig-promisedeployresult)
- [`client.deployProject(deployConfig: DeployProjectConfig): Promise<DeployResult>`](#clientdeployprojectdeployconfig-deployprojectconfig-promisedeployresult)
- [`client.getClient(): GotClient`](#clientgetclient-gotclient)
- [`client.list(listConfig: ListConfig): Promise<ListResult>`](#clientlistlistconfig-listconfig-promiselistresult)
- [`api` and `fsHelpers`](#api-and-fshelpers)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [Contributors](#contributors)
- [License](#license)
* [Installation](#installation)
* [Example](#example)
* [HTTP Client Configuration](#http-client-configuration)
* [API](#api)
* [`client.activateBuild(activateConfig: ActivateConfig): Promise<ActivateResult>`](#clientactivatebuildactivateconfig-activateconfig-promiseactivateresult)
* [`client.deployLocalProject(deployConfig: DeployLocalProjectConfig): Promise<DeployResult>`](#clientdeploylocalprojectdeployconfig-deploylocalprojectconfig-promisedeployresult)
* [`client.deployProject(deployConfig: DeployProjectConfig): Promise<DeployResult>`](#clientdeployprojectdeployconfig-deployprojectconfig-promisedeployresult)
* [`client.getClient(): GotClient`](#clientgetclient-gotclient)
* [`client.list(listConfig: ListConfig): Promise<ListResult>`](#clientlistlistconfig-listconfig-promiselistresult)
* [`api` and `fsHelpers`](#api-and-fshelpers)
* [Contributing](#contributing)
* [Code of Conduct](#code-of-conduct)
* [Contributors](#contributors)
* [License](#license)

## Installation

Expand Down Expand Up @@ -56,6 +57,28 @@ const result = await client.deployLocalProject({
});
```

## HTTP Client Configuration

When deploying lots of Functions and Assets it is possible to run up against the enforced concurrency limits of the Twilio API. You can limit the concurrency and set how many times the library retries API requests either in the constructor for `TwilioServerlessApiClient` or using environment variables (useful when this is part of a CLI tool like `twilio-run`).

The default concurrency is 50 and the default number of retries is 10. You can change this in the config, the following would set concurrency to 1, only 1 live request at a time, and retries to 0, so if it fails it won't retry.

```js
const client = new TwilioServerlessApiClient({
accountSid: '...',
authToken: '...',
concurrency: 1,
retryLimit: 0
};);
```

You can also set these values with the following environment variables:

```bash
export TWILIO_SERVERLESS_API_CONCURRENCY=1
export TWILIO_SERVERLESS_API_RETRY_LIMIT=0
```

## API

You can find the full reference documentation of everything at: https://serverless-api.twilio-labs.com
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"form-data": "^2.5.0",
"got": "^11.0.1",
"mime-types": "^2.1.22",
"p-limit": "^2.3.0",
"recursive-readdir": "^2.2.2",
"type-fest": "^0.3.0",
"upath": "^1.1.2"
Expand Down
3 changes: 0 additions & 3 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ describe('createGotClient', () => {
expect((options as any).password).toBe(
DEFAULT_TEST_CLIENT_CONFIG.authToken
);
expect(client.twilioClientConfig).toEqual(config);
});

test('works with region configuration', () => {
Expand All @@ -32,7 +31,6 @@ describe('createGotClient', () => {
expect((options as any).password).toBe(
DEFAULT_TEST_CLIENT_CONFIG.authToken
);
expect(client.twilioClientConfig).toEqual(config);
});

test('works with region & edge configuration', () => {
Expand All @@ -53,6 +51,5 @@ describe('createGotClient', () => {
expect((options as any).password).toBe(
DEFAULT_TEST_CLIENT_CONFIG.authToken
);
expect(client.twilioClientConfig).toEqual(config);
});
});
41 changes: 24 additions & 17 deletions src/api/assets.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/** @module @twilio-labs/serverless-api/dist/api */

const { promisfy } = require('util');

import debug from 'debug';
import FormData from 'form-data';
import {
AssetApiResource,
AssetList,
AssetResource,
GotClient,
ServerlessResourceConfig,
Sid,
VersionResource,
ClientConfig,
} from '../types';
import { TwilioServerlessApiClient } from '../client';
import { getContentType } from '../utils/content-type';
import { ClientApiError } from '../utils/error';
import { getApiUrl } from './utils/api-client';
Expand All @@ -25,16 +24,16 @@ const log = debug('twilio-serverless-api:assets');
*
* @param {string} name friendly name of the resource
* @param {string} serviceSid service to register asset under
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<AssetApiResource>}
*/
async function createAssetResource(
name: string,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<AssetApiResource> {
try {
const resp = await client.post(`Services/${serviceSid}/Assets`, {
const resp = await client.request('post', `Services/${serviceSid}/Assets`, {
form: {
FriendlyName: name,
},
Expand All @@ -50,12 +49,12 @@ async function createAssetResource(
* Calls the API to retrieve a list of all assets
*
* @param {string} serviceSid service to look for assets
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<AssetApiResource[]>}
*/
export async function listAssetResources(
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
) {
try {
return getPaginatedResource<AssetList, AssetApiResource>(
Expand All @@ -73,13 +72,13 @@ export async function listAssetResources(
*
* @param {FileInfo[]} assets
* @param {string} serviceSid
* @param {GotClient} client
* @param {TwilioServerlessApiClient} client
* @returns {Promise<AssetResource[]>}
*/
export async function getOrCreateAssetResources(
assets: ServerlessResourceConfig[],
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<AssetResource[]> {
const output: AssetResource[] = [];
const existingAssets = await listAssetResources(serviceSid, client);
Expand Down Expand Up @@ -121,13 +120,14 @@ export async function getOrCreateAssetResources(
*
* @param {AssetResource} asset the one to create a new version for
* @param {string} serviceSid the service to create the asset version for
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<VersionResource>}
*/
async function createAssetVersion(
asset: AssetResource,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient,
clientConfig: ClientConfig
): Promise<VersionResource> {
try {
const contentType = await getContentType(
Expand All @@ -146,11 +146,12 @@ async function createAssetVersion(
form.append('Visibility', asset.access);
form.append('Content', asset.content, contentOpts);

const resp = await client.post(
const resp = await client.request(
'post',
`Services/${serviceSid}/Assets/${asset.sid}/Versions`,
{
responseType: 'text',
prefixUrl: getApiUrl(client.twilioClientConfig, 'serverless-upload'),
prefixUrl: getApiUrl(clientConfig, 'serverless-upload'),
body: form,
}
);
Expand All @@ -168,14 +169,20 @@ async function createAssetVersion(
* @export
* @param {AssetResource} asset The asset to upload
* @param {string} serviceSid The service to upload it to
* @param {GotClient} client The API client
* @param {TwilioServerlessApiClient} client The API client
* @returns {Promise<Sid>}
*/
export async function uploadAsset(
asset: AssetResource,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient,
clientConfig: ClientConfig
): Promise<Sid> {
const version = await createAssetVersion(asset, serviceSid, client);
const version = await createAssetVersion(
asset,
serviceSid,
client,
clientConfig
);
return version.sid;
}
63 changes: 29 additions & 34 deletions src/api/builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@

import debug from 'debug';
import querystring, { ParsedUrlQueryInput } from 'querystring';
import {
BuildConfig,
BuildList,
BuildResource,
BuildStatus,
GotClient,
} from '../types';
import { BuildConfig, BuildList, BuildResource, BuildStatus } from '../types';
import { TwilioServerlessApiClient } from '../client';
import { DeployStatus } from '../types/consts';
import { ClientApiError } from '../utils/error';
import { sleep } from '../utils/sleep';
Expand All @@ -24,15 +19,18 @@ const log = debug('twilio-serverless-api:builds');
* @export
* @param {string} buildSid SID of build to retrieve
* @param {string} serviceSid service to retrieve build from
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<BuildResource>}
*/
export async function getBuild(
buildSid: string,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<BuildResource> {
const resp = await client.get(`Services/${serviceSid}/Builds/${buildSid}`);
const resp = await client.request(
'get',
`Services/${serviceSid}/Builds/${buildSid}`
);
return (resp.body as unknown) as BuildResource;
}

Expand All @@ -41,13 +39,13 @@ export async function getBuild(
*
* @param {string} buildSid the SID of the build
* @param {string} serviceSid the SID of the service the build belongs to
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<BuildStatus>}
*/
async function getBuildStatus(
buildSid: string,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<BuildStatus> {
try {
const resp = await getBuild(buildSid, serviceSid, client);
Expand All @@ -63,12 +61,12 @@ async function getBuildStatus(
*
* @export
* @param {string} serviceSid the SID of the service
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<BuildResource[]>}
*/
export async function listBuilds(
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<BuildResource[]> {
return getPaginatedResource<BuildList, BuildResource>(
client,
Expand All @@ -82,13 +80,13 @@ export async function listBuilds(
* @export
* @param {BuildConfig} config build-related information (functions, assets, dependencies)
* @param {string} serviceSid the service to create the build for
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<BuildResource>}
*/
export async function triggerBuild(
config: BuildConfig,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<BuildResource> {
const { functionVersions, dependencies, assetVersions } = config;
try {
Expand All @@ -107,7 +105,7 @@ export async function triggerBuild(
body.AssetVersions = assetVersions;
}

const resp = await client.post(`Services/${serviceSid}/Builds`, {
const resp = await client.request('post', `Services/${serviceSid}/Builds`, {
responseType: 'json',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -127,15 +125,15 @@ export async function triggerBuild(
* @export
* @param {string} buildSid the build to wait for
* @param {string} serviceSid the service of the build
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @param {events.EventEmitter} eventEmitter optional event emitter to communicate current build status
* @param {number} timeout optional timeout. default: 5 minutes
* @returns {Promise<void>}
*/
export function waitForSuccessfulBuild(
buildSid: string,
serviceSid: string,
client: GotClient,
client: TwilioServerlessApiClient,
eventEmitter: events.EventEmitter,
timeout: number = 5 * 60 * 1000
): Promise<void> {
Expand All @@ -145,12 +143,10 @@ export function waitForSuccessfulBuild(

while (!isBuilt) {
if (Date.now() - startTime > timeout) {
if (eventEmitter) {
eventEmitter.emit('status-update', {
status: DeployStatus.TIMED_OUT,
message: 'Deployment took too long',
});
}
eventEmitter.emit('status-update', {
status: DeployStatus.TIMED_OUT,
message: 'Deployment took too long',
});
reject(new Error('Timeout'));
}
const status = await getBuildStatus(buildSid, serviceSid, client);
Expand All @@ -165,12 +161,10 @@ export function waitForSuccessfulBuild(
return;
}

if (eventEmitter) {
eventEmitter.emit('status-update', {
status: DeployStatus.BUILDING,
message: `Waiting for deployment. Current status: ${status}`,
});
}
eventEmitter.emit('status-update', {
status: DeployStatus.BUILDING,
message: `Waiting for deployment. Current status: ${status}`,
});
await sleep(1000);
}
resolve();
Expand All @@ -184,17 +178,18 @@ export function waitForSuccessfulBuild(
* @param {string} buildSid the build to be activated
* @param {string} environmentSid the target environment for the build to be deployed to
* @param {string} serviceSid the service of the project
* @param {GotClient} client API client
* @param {TwilioServerlessApiClient} client API client
* @returns {Promise<any>}
*/
export async function activateBuild(
buildSid: string,
environmentSid: string,
serviceSid: string,
client: GotClient
client: TwilioServerlessApiClient
): Promise<any> {
try {
const resp = await client.post(
const resp = await client.request(
'post',
`Services/${serviceSid}/Environments/${environmentSid}/Deployments`,
{
form: {
Expand Down
Loading