diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 71d0c4488..611176eb6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,11 +4,12 @@ "dev-packages/logging-utils": "1.1.3", "dev-packages/pack-n-play": "4.2.1", "generator/gapic-generator-typescript": "4.11.10", - "packages/google-auth-library-nodejs": "10.5.0", "packages/gax": "5.0.6", "packages/gaxios": "7.1.3", "packages/gcp-metadata": "8.1.2", + "packages/google-auth-library-nodejs": "10.5.0", "packages/nodejs-googleapis-common": "8.0.1", "packages/proto3-json-serializer-nodejs": "3.0.4", + "packages/retry-request": "8.0.2", "packages/tools": "1.0.5" } diff --git a/packages/gax/package.json b/packages/gax/package.json index 27b21bfb6..26cb0367e 100644 --- a/packages/gax/package.json +++ b/packages/gax/package.json @@ -19,7 +19,7 @@ "object-hash": "^3.0.0", "proto3-json-serializer": "3.0.4", "protobufjs": "^7.5.3", - "retry-request": "^8.0.0", + "retry-request": "8.0.2", "rimraf": "^5.0.1" }, "devDependencies": { diff --git a/packages/retry-request/.circleci/config.yml b/packages/retry-request/.circleci/config.yml new file mode 100644 index 000000000..c4e57a26b --- /dev/null +++ b/packages/retry-request/.circleci/config.yml @@ -0,0 +1,27 @@ +version: 2.1 + +install_and_test: &install_and_test + steps: + - checkout + - run: + name: Install + command: npm install + - run: + name: Test + command: npm test + +jobs: + test_node10: + docker: + - image: circleci/node:10 + <<: *install_and_test + test_node12: + docker: + - image: circleci/node:12 + <<: *install_and_test + +workflows: + test: + jobs: + - test_node10 + - test_node12 diff --git a/packages/retry-request/.eslintignore b/packages/retry-request/.eslintignore new file mode 100644 index 000000000..c4a0963e9 --- /dev/null +++ b/packages/retry-request/.eslintignore @@ -0,0 +1,8 @@ +**/node_modules +**/coverage +test/fixtures +build/ +docs/ +protos/ +samples/generated/ +system-test/**/fixtures diff --git a/packages/retry-request/.eslintrc.json b/packages/retry-request/.eslintrc.json new file mode 100644 index 000000000..782153495 --- /dev/null +++ b/packages/retry-request/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts" +} diff --git a/packages/retry-request/.gitattributes b/packages/retry-request/.gitattributes new file mode 100644 index 000000000..33739cb74 --- /dev/null +++ b/packages/retry-request/.gitattributes @@ -0,0 +1,4 @@ +*.ts text eol=lf +*.js text eol=lf +protos/* linguist-generated +**/api-extractor.json linguist-language=JSON-with-Comments diff --git a/packages/retry-request/.github/.OwlBot.yaml b/packages/retry-request/.github/.OwlBot.yaml new file mode 100644 index 000000000..ae4a11c98 --- /dev/null +++ b/packages/retry-request/.github/.OwlBot.yaml @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +docker: + image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest diff --git a/packages/retry-request/.gitignore b/packages/retry-request/.gitignore new file mode 100644 index 000000000..5d32b2378 --- /dev/null +++ b/packages/retry-request/.gitignore @@ -0,0 +1,14 @@ +**/*.log +**/node_modules +.coverage +coverage +.nyc_output +docs/ +out/ +build/ +system-test/secrets.js +system-test/*key.json +*.lock +.DS_Store +package-lock.json +__pycache__ diff --git a/packages/retry-request/.jsdoc.js b/packages/retry-request/.jsdoc.js new file mode 100644 index 000000000..71eef870c --- /dev/null +++ b/packages/retry-request/.jsdoc.js @@ -0,0 +1,55 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +'use strict'; + +module.exports = { + opts: { + readme: './readme.md', + package: './package.json', + template: './node_modules/jsdoc-fresh', + recurse: true, + verbose: true, + destination: './docs/' + }, + plugins: [ + 'plugins/markdown', + 'jsdoc-region-tag' + ], + source: { + excludePattern: '(^|\\/|\\\\)[._]', + include: [ + 'index.js', + 'index.d.ts' + ], + includePattern: '\\.js$' + }, + templates: { + copyright: 'Copyright 2022 Google LLC', + includeDate: false, + sourceFiles: false, + systemName: 'retry-request', + theme: 'lumen', + default: { + outputSourceFiles: false + } + }, + markdown: { + idInHeadings: true + } +}; diff --git a/packages/retry-request/.jshintrc b/packages/retry-request/.jshintrc new file mode 100644 index 000000000..438850dbc --- /dev/null +++ b/packages/retry-request/.jshintrc @@ -0,0 +1,26 @@ +{ + "bitwise": true, + "curly": true, + "eqeqeq": true, + "esnext": true, + "freeze": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "node": true, + "noarg": true, + "quotmark": "single", + "strict": true, + "trailing": true, + "undef": true, + "unused": true, + "globals": { + "describe": true, + "it": true, + "before": true, + "after": true, + "beforeEach": true, + "afterEach": true + } +} diff --git a/packages/retry-request/.mocharc.js b/packages/retry-request/.mocharc.js new file mode 100644 index 000000000..0b600509b --- /dev/null +++ b/packages/retry-request/.mocharc.js @@ -0,0 +1,29 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +const config = { + "enable-source-maps": true, + "throw-deprecation": true, + "timeout": 10000, + "recursive": true +} +if (process.env.MOCHA_THROW_DEPRECATION === 'false') { + delete config['throw-deprecation']; +} +if (process.env.MOCHA_REPORTER) { + config.reporter = process.env.MOCHA_REPORTER; +} +if (process.env.MOCHA_REPORTER_OUTPUT) { + config['reporter-option'] = `output=${process.env.MOCHA_REPORTER_OUTPUT}`; +} +module.exports = config diff --git a/packages/retry-request/.nycrc b/packages/retry-request/.nycrc new file mode 100644 index 000000000..b18d5472b --- /dev/null +++ b/packages/retry-request/.nycrc @@ -0,0 +1,24 @@ +{ + "report-dir": "./.coverage", + "reporter": ["text", "lcov"], + "exclude": [ + "**/*-test", + "**/.coverage", + "**/apis", + "**/benchmark", + "**/conformance", + "**/docs", + "**/samples", + "**/scripts", + "**/protos", + "**/test", + "**/*.d.ts", + ".jsdoc.js", + "**/.jsdoc.js", + "karma.conf.js", + "webpack-tests.config.js", + "webpack.config.js" + ], + "exclude-after-remap": false, + "all": true +} diff --git a/packages/retry-request/.prettierignore b/packages/retry-request/.prettierignore new file mode 100644 index 000000000..9340ad9b8 --- /dev/null +++ b/packages/retry-request/.prettierignore @@ -0,0 +1,6 @@ +**/node_modules +**/coverage +test/fixtures +build/ +docs/ +protos/ diff --git a/packages/retry-request/.prettierrc.js b/packages/retry-request/.prettierrc.js new file mode 100644 index 000000000..d1b95106f --- /dev/null +++ b/packages/retry-request/.prettierrc.js @@ -0,0 +1,17 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = { + ...require('gts/.prettierrc.json') +} diff --git a/packages/retry-request/.readme-partials.yaml b/packages/retry-request/.readme-partials.yaml new file mode 100644 index 000000000..8d9a6b1bb --- /dev/null +++ b/packages/retry-request/.readme-partials.yaml @@ -0,0 +1,4 @@ +introduction: |- + **_THIS REPOSITORY AND PACKAGE WILL BE DEPRECATED IN 2025. RELEVANT FUNCTIONALITIES HAVE BEEN MOVED TO [GOOGLEAPIS/GAXIOS](https://github.com/googleapis/gaxios)_** + + Retry a request with built-in exponential backoff. diff --git a/packages/retry-request/.repo-metadata.json b/packages/retry-request/.repo-metadata.json new file mode 100644 index 000000000..5915a8867 --- /dev/null +++ b/packages/retry-request/.repo-metadata.json @@ -0,0 +1,7 @@ +{ + "library_type": "OTHER", + "release_level": "stable", + "client_documentation": "https://github.com/googleapis/retry-request", + "codeowner_team": "@googleapis/jsteam-handwritten-libraries", + "repo": "googleapis/google-cloud-node-core" +} diff --git a/packages/retry-request/CHANGELOG.md b/packages/retry-request/CHANGELOG.md new file mode 100644 index 000000000..3d0cea40d --- /dev/null +++ b/packages/retry-request/CHANGELOG.md @@ -0,0 +1,116 @@ +# Changelog + +## [8.0.2](https://github.com/googleapis/retry-request/compare/v8.0.1...v8.0.2) (2025-08-05) + + +### Bug Fixes + +* Move"@types/request" to dev ([a42f283](https://github.com/googleapis/retry-request/commit/a42f283ad3d30106f542e7d9312f13182b805209)) + +## [8.0.1](https://github.com/googleapis/retry-request/compare/v8.0.0...v8.0.1) (2025-08-05) + + +### Bug Fixes + +* Resolve vulnerability ([#150](https://github.com/googleapis/retry-request/issues/150)) ([5a446a1](https://github.com/googleapis/retry-request/commit/5a446a18cdd06dcdeda4575d4086b8bb86e16353)) + +## [8.0.0](https://github.com/googleapis/retry-request/compare/v7.0.2...v8.0.0) (2025-02-19) + + +### ⚠ BREAKING CHANGES + +* upgrade to Node 18 ([#137](https://github.com/googleapis/retry-request/issues/137)) + +### Miscellaneous Chores + +* Upgrade to Node 18 ([#137](https://github.com/googleapis/retry-request/issues/137)) ([5ec12e5](https://github.com/googleapis/retry-request/commit/5ec12e55b0c2aa3d178c1f9351537e5b96663427)) + +## [7.0.2](https://github.com/googleapis/retry-request/compare/v7.0.1...v7.0.2) (2024-01-19) + + +### Bug Fixes + +* Get rid of debug runtime dependency ([#118](https://github.com/googleapis/retry-request/issues/118)) ([85e1bd0](https://github.com/googleapis/retry-request/commit/85e1bd0a46630876757344405a6c8620d9de6451)) + +## [7.0.1](https://github.com/googleapis/retry-request/compare/v7.0.0...v7.0.1) (2023-10-12) + + +### Bug Fixes + +* Types ([#109](https://github.com/googleapis/retry-request/issues/109)) ([35c0260](https://github.com/googleapis/retry-request/commit/35c02608138a4b66cf488e4e1e90cbff05d42c2f)) + +## [7.0.0](https://github.com/googleapis/retry-request/compare/v6.0.0...v7.0.0) (2023-10-11) + + +### ⚠ BREAKING CHANGES + +* Remove `request` fallback, Update Docs, Update Types ([#107](https://github.com/googleapis/retry-request/issues/107)) + +### Features + +* Remove `request` fallback, Update Docs, Update Types ([#107](https://github.com/googleapis/retry-request/issues/107)) ([ba8abb2](https://github.com/googleapis/retry-request/commit/ba8abb242ff21921a81e3178081806e95a902a68)) + +## [6.0.0](https://github.com/googleapis/retry-request/compare/v5.0.2...v6.0.0) (2023-07-20) + + +### ⚠ BREAKING CHANGES + +* Move to node 14 as the minimum version ([#97](https://github.com/googleapis/retry-request/issues/97)) +* move minimum version to node 14 ([#96](https://github.com/googleapis/retry-request/issues/96)) + +### Features + +* Move to node 14 as the minimum version ([#97](https://github.com/googleapis/retry-request/issues/97)) ([f42d074](https://github.com/googleapis/retry-request/commit/f42d0743de048fdf46c7f480f3dec59c2caefa5a)) + + +### Miscellaneous Chores + +* Move minimum version to node 14 ([#96](https://github.com/googleapis/retry-request/issues/96)) ([2167a91](https://github.com/googleapis/retry-request/commit/2167a91c36e95522ad7e7293b4744ddd81c6166e)) + +## [5.0.2](https://github.com/googleapis/retry-request/compare/v5.0.1...v5.0.2) (2022-09-14) + + +### Bug Fixes + +* Delete second LICENSE ([#82](https://github.com/googleapis/retry-request/issues/82)) ([9dfaf18](https://github.com/googleapis/retry-request/commit/9dfaf1819b18b6d660924951b9cf1c509b6e9870)) +* Remove pip install statements ([#1546](https://github.com/googleapis/retry-request/issues/1546)) ([#79](https://github.com/googleapis/retry-request/issues/79)) ([cb4e15d](https://github.com/googleapis/retry-request/commit/cb4e15d6e8ae9d72855ed1071957afe9d301b12e)) + +## [5.0.1](https://github.com/googleapis/retry-request/compare/v5.0.0...v5.0.1) (2022-06-09) + + +### Bug Fixes + +* respect totalTimeout and do not retry if nextRetryDelay is <= 0 ([#38](https://github.com/googleapis/retry-request/issues/38)) ([9501a42](https://github.com/googleapis/retry-request/commit/9501a42d06a620282dcd2ff9990fd0b5033a990b)) + +## [5.0.0](https://github.com/googleapis/retry-request/compare/v4.2.2...v5.0.0) (2022-05-06) + + +### ⚠ BREAKING CHANGES + +* drop node 10 (#68) + +### Build System + +* drop node 10 ([#68](https://github.com/googleapis/retry-request/issues/68)) ([00ec90c](https://github.com/googleapis/retry-request/commit/00ec90c4d3cb29245ca746e0e133fcddc22d2251)) + +### [4.2.1](https://github.com/googleapis/retry-request/compare/v4.2.0...v4.2.1) (2022-04-12) + + +### Bug Fixes + +* add new retry options to types ([#36](https://github.com/googleapis/retry-request/issues/36)) ([3f10798](https://github.com/googleapis/retry-request/commit/3f10798f47c03b50f1ba352b04d09ea3d0458b9c)) +* use extend instead of object.assign ([#37](https://github.com/googleapis/retry-request/issues/37)) ([8c8dcdd](https://github.com/googleapis/retry-request/commit/8c8dcdd7d6262ce305c93fa4a8a7b2630e984824)) + +## [4.2.0](https://github.com/googleapis/retry-request/compare/v4.1.0...v4.2.0) (2022-04-06) + + +### Features + +* support enhanced retry settings ([#35](https://github.com/googleapis/retry-request/issues/35)) ([0184676](https://github.com/googleapis/retry-request/commit/0184676dee36596fb939fb4559af11d0a14f64bd)) + + +### Bug Fixes + +* add new retry options to types ([#36](https://github.com/googleapis/retry-request/issues/36)) ([3f10798](https://github.com/googleapis/retry-request/commit/3f10798f47c03b50f1ba352b04d09ea3d0458b9c)) +* correctly calculate retry attempt ([#33](https://github.com/googleapis/retry-request/issues/33)) ([4c852e2](https://github.com/googleapis/retry-request/commit/4c852e2ba22a7f75edfb3c905bd37a7e9913e67d)) +* use extend instead of object.assign ([#37](https://github.com/googleapis/retry-request/issues/37)) ([8c8dcdd](https://github.com/googleapis/retry-request/commit/8c8dcdd7d6262ce305c93fa4a8a7b2630e984824)) diff --git a/packages/retry-request/CODE_OF_CONDUCT.md b/packages/retry-request/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..2add2547a --- /dev/null +++ b/packages/retry-request/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ + +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/packages/retry-request/CONTRIBUTING.md b/packages/retry-request/CONTRIBUTING.md new file mode 100644 index 000000000..72c44cada --- /dev/null +++ b/packages/retry-request/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# How to become a contributor and submit your own code + +**Table of contents** + +* [Contributor License Agreements](#contributor-license-agreements) +* [Contributing a patch](#contributing-a-patch) +* [Running the tests](#running-the-tests) +* [Releasing the library](#releasing-the-library) + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the code to which + you are contributing. +1. Ensure that your code has an appropriate set of tests which all pass. +1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. +1. Submit a pull request. + +### Before you begin + +1. [Select or create a Cloud Platform project][projects]. +1. [Set up authentication with a service account][auth] so you can access the + API from your local workstation. + + +## Running the tests + +1. [Prepare your environment for Node.js setup][setup]. + +1. Install dependencies: + + npm install + +1. Run the tests: + + # Run unit tests. + npm test + + # Run sample integration tests. + npm run samples-test + + # Run all system tests. + npm run system-test + +1. Lint (and maybe fix) any changes: + + npm run fix + +[setup]: https://cloud.google.com/nodejs/docs/setup +[projects]: https://console.cloud.google.com/project +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing + +[auth]: https://cloud.google.com/docs/authentication/getting-started \ No newline at end of file diff --git a/packages/retry-request/index.d.ts b/packages/retry-request/index.d.ts new file mode 100644 index 000000000..41729d099 --- /dev/null +++ b/packages/retry-request/index.d.ts @@ -0,0 +1,42 @@ +declare module 'retry-request' { + import * as request from 'request'; + import * as teenyRequest from 'teeny-request'; + + type teenyRequestFunction = typeof teenyRequest extends Function + ? typeof teenyRequest + : never; + + namespace retryRequest { + /** + * Set the defaults for `retryRequest`. + */ + const defaults: retryRequest.Options; + /** + * Determines the next retry based on the provided configuration. + */ + function getNextRetryDelay(config: Options): number; + interface Options { + objectMode?: boolean; + request: typeof request | teenyRequestFunction; + retries?: number; + noResponseRetries?: number; + currentRetryAttempt?: number; + maxRetryDelay?: number; + retryDelayMultiplier?: number; + totalTimeout?: number; + shouldRetryFn?: (response: request.RequestResponse) => boolean; + } + } + + function retryRequest( + requestOpts: request.Options, + opts: retryRequest.Options, + callback?: request.RequestCallback, + ): {abort: () => void}; + function retryRequest( + requestOpts: request.Options, + callback?: request.RequestCallback, + ): {abort: () => void}; + + export = retryRequest; +} diff --git a/packages/retry-request/index.js b/packages/retry-request/index.js new file mode 100644 index 000000000..298a35109 --- /dev/null +++ b/packages/retry-request/index.js @@ -0,0 +1,293 @@ +'use strict'; + +const {PassThrough} = require('stream'); +const extend = require('extend'); + +let debug = () => {}; +if ( + typeof process !== 'undefined' && + 'env' in process && + typeof process.env === 'object' && + process.env.DEBUG === 'retry-request' +) { + debug = message => { + console.log('retry-request:', message); + }; +} + +const DEFAULTS = { + objectMode: false, + retries: 2, + + /* + The maximum time to delay in seconds. If retryDelayMultiplier results in a + delay greater than maxRetryDelay, retries should delay by maxRetryDelay + seconds instead. + */ + maxRetryDelay: 64, + + /* + The multiplier by which to increase the delay time between the completion of + failed requests, and the initiation of the subsequent retrying request. + */ + retryDelayMultiplier: 2, + + /* + The length of time to keep retrying in seconds. The last sleep period will + be shortened as necessary, so that the last retry runs at deadline (and not + considerably beyond it). The total time starting from when the initial + request is sent, after which an error will be returned, regardless of the + retrying attempts made meanwhile. + */ + totalTimeout: 600, + + noResponseRetries: 2, + currentRetryAttempt: 0, + shouldRetryFn: function (response) { + const retryRanges = [ + // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + // 1xx - Retry (Informational, request still processing) + // 2xx - Do not retry (Success) + // 3xx - Do not retry (Redirect) + // 4xx - Do not retry (Client errors) + // 429 - Retry ("Too Many Requests") + // 5xx - Retry (Server errors) + [100, 199], + [429, 429], + [500, 599], + ]; + + const statusCode = response.statusCode; + debug(`Response status: ${statusCode}`); + + let range; + while ((range = retryRanges.shift())) { + if (statusCode >= range[0] && statusCode <= range[1]) { + // Not a successful status or redirect. + return true; + } + } + }, +}; + +function retryRequest(requestOpts, opts, callback) { + if (typeof requestOpts === 'string') { + requestOpts = {url: requestOpts}; + } + + const streamMode = typeof arguments[arguments.length - 1] !== 'function'; + + if (typeof opts === 'function') { + callback = opts; + } + + const manualCurrentRetryAttemptWasSet = + opts && typeof opts.currentRetryAttempt === 'number'; + opts = extend({}, DEFAULTS, opts); + + if (typeof opts.request === 'undefined') { + throw new Error('A request library must be provided to retry-request.'); + } + + let currentRetryAttempt = opts.currentRetryAttempt; + + let numNoResponseAttempts = 0; + let streamResponseHandled = false; + + let retryStream; + let requestStream; + let delayStream; + + let activeRequest; + const retryRequest = { + abort: function () { + if (activeRequest && activeRequest.abort) { + activeRequest.abort(); + } + }, + }; + + if (streamMode) { + retryStream = new PassThrough({objectMode: opts.objectMode}); + retryStream.abort = resetStreams; + } + + const timeOfFirstRequest = Date.now(); + if (currentRetryAttempt > 0) { + retryAfterDelay(currentRetryAttempt); + } else { + makeRequest(); + } + + if (streamMode) { + return retryStream; + } else { + return retryRequest; + } + + function resetStreams() { + delayStream = null; + + if (requestStream) { + requestStream.abort && requestStream.abort(); + requestStream.cancel && requestStream.cancel(); + + if (requestStream.destroy) { + requestStream.destroy(); + } else if (requestStream.end) { + requestStream.end(); + } + } + } + + function makeRequest() { + let finishHandled = false; + currentRetryAttempt++; + debug(`Current retry attempt: ${currentRetryAttempt}`); + + function handleFinish(args = []) { + if (!finishHandled) { + finishHandled = true; + retryStream.emit('complete', ...args); + } + } + + if (streamMode) { + streamResponseHandled = false; + + delayStream = new PassThrough({objectMode: opts.objectMode}); + requestStream = opts.request(requestOpts); + + setImmediate(() => { + retryStream.emit('request'); + }); + + requestStream + // gRPC via google-cloud-node can emit an `error` as well as a `response` + // Whichever it emits, we run with-- we can't run with both. That's what + // is up with the `streamResponseHandled` tracking. + .on('error', err => { + if (streamResponseHandled) { + return; + } + + streamResponseHandled = true; + onResponse(err); + }) + .on('response', (resp, body) => { + if (streamResponseHandled) { + return; + } + + streamResponseHandled = true; + onResponse(null, resp, body); + }) + .on('complete', (...params) => handleFinish(params)) + .on('finish', (...params) => handleFinish(params)); + + requestStream.pipe(delayStream); + } else { + activeRequest = opts.request(requestOpts, onResponse); + } + } + + function retryAfterDelay(currentRetryAttempt) { + if (streamMode) { + resetStreams(); + } + + const nextRetryDelay = getNextRetryDelay({ + maxRetryDelay: opts.maxRetryDelay, + retryDelayMultiplier: opts.retryDelayMultiplier, + retryNumber: currentRetryAttempt, + timeOfFirstRequest, + totalTimeout: opts.totalTimeout, + }); + debug(`Next retry delay: ${nextRetryDelay}`); + + if (nextRetryDelay <= 0) { + numNoResponseAttempts = opts.noResponseRetries + 1; + return; + } + + setTimeout(makeRequest, nextRetryDelay); + } + + function onResponse(err, response, body) { + // An error such as DNS resolution. + if (err) { + numNoResponseAttempts++; + + if (numNoResponseAttempts <= opts.noResponseRetries) { + retryAfterDelay(numNoResponseAttempts); + } else { + if (streamMode) { + retryStream.emit('error', err); + retryStream.end(); + } else { + callback(err, response, body); + } + } + + return; + } + + // Send the response to see if we should try again. + // NOTE: "currentRetryAttempt" isn't accurate by default, as it counts + // the very first request sent as the first "retry". It is only accurate + // when a user provides their own "currentRetryAttempt" option at + // instantiation. + const adjustedCurrentRetryAttempt = manualCurrentRetryAttemptWasSet + ? currentRetryAttempt + : currentRetryAttempt - 1; + if ( + adjustedCurrentRetryAttempt < opts.retries && + opts.shouldRetryFn(response) + ) { + retryAfterDelay(currentRetryAttempt); + return; + } + + // No more attempts need to be made, just continue on. + if (streamMode) { + retryStream.emit('response', response); + delayStream.pipe(retryStream); + requestStream.on('error', err => { + retryStream.destroy(err); + }); + } else { + callback(err, response, body); + } + } +} + +module.exports = retryRequest; + +function getNextRetryDelay(config) { + const { + maxRetryDelay, + retryDelayMultiplier, + retryNumber, + timeOfFirstRequest, + totalTimeout, + } = config; + + const maxRetryDelayMs = maxRetryDelay * 1000; + const totalTimeoutMs = totalTimeout * 1000; + + const jitter = Math.floor(Math.random() * 1000); + const calculatedNextRetryDelay = + Math.pow(retryDelayMultiplier, retryNumber) * 1000 + jitter; + + const maxAllowableDelayMs = + totalTimeoutMs - (Date.now() - timeOfFirstRequest); + + return Math.min( + calculatedNextRetryDelay, + maxAllowableDelayMs, + maxRetryDelayMs, + ); +} + +module.exports.defaults = DEFAULTS; +module.exports.getNextRetryDelay = getNextRetryDelay; diff --git a/packages/retry-request/license b/packages/retry-request/license new file mode 100644 index 000000000..854d86a69 --- /dev/null +++ b/packages/retry-request/license @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Stephen Sawchuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/retry-request/linkinator.config.json b/packages/retry-request/linkinator.config.json new file mode 100644 index 000000000..12d77fa67 --- /dev/null +++ b/packages/retry-request/linkinator.config.json @@ -0,0 +1,12 @@ +{ + "recurse": true, + "skip": [ + "/logo.png" + ], + "silent": true, + "concurrency": 5, + "retry": true, + "retryErrors": true, + "retryErrorsCount": 5, + "retryErrorsJitter": 3000 + } \ No newline at end of file diff --git a/packages/retry-request/logo.png b/packages/retry-request/logo.png new file mode 100644 index 000000000..7a767e80e Binary files /dev/null and b/packages/retry-request/logo.png differ diff --git a/packages/retry-request/owlbot.py b/packages/retry-request/owlbot.py new file mode 100644 index 000000000..09ee91517 --- /dev/null +++ b/packages/retry-request/owlbot.py @@ -0,0 +1,20 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import synthtool.languages.node as node +import logging + +logging.basicConfig(level=logging.DEBUG) + +node.owlbot_main(templates_excludes=["README.md", "LICENSE", ".github/workflows/response.yaml", ".github/scripts", ".github/ISSUE_TEMPLATE"]) diff --git a/packages/retry-request/package.json b/packages/retry-request/package.json new file mode 100644 index 000000000..0925b3c43 --- /dev/null +++ b/packages/retry-request/package.json @@ -0,0 +1,54 @@ +{ + "name": "retry-request", + "version": "8.0.2", + "description": "Retry a request.", + "main": "index.js", + "repository": { + "type": "git", + "directory": "packages/retry-request", + "url": "https://github.com/googleapis/google-cloud-node-core.git" + }, + "scripts": { + "docs": "jsdoc -c .jsdoc.js", + "predocs-test": "npm run docs", + "docs-test": "linkinator docs", + "fix": "gts fix", + "lint": "gts check", + "test": "mocha --timeout 30000", + "system-test": "echo no system test", + "samples-test": "echo no samples" + }, + "files": [ + "index.js", + "index.d.ts", + "license" + ], + "types": "index.d.ts", + "keywords": [ + "request", + "retry", + "stream" + ], + "author": "Stephen Sawchuk ", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "devDependencies": { + "@types/request": "^2.48.13", + "async": "^3.2.6", + "gts": "^6.0.2", + "jsdoc": "^4.0.4", + "jsdoc-fresh": "^5.0.0", + "jsdoc-region-tag": "^4.0.0", + "linkinator": "^6.1.2", + "lodash.range": "^3.2.0", + "mocha": "^11.1.0", + "typescript": "5.8.3" + }, + "homepage": "https://github.com/googleapis/google-cloud-node-core/tree/main/packages/retry-request" +} diff --git a/packages/retry-request/readme.md b/packages/retry-request/readme.md new file mode 100644 index 000000000..9f53e21d6 --- /dev/null +++ b/packages/retry-request/readme.md @@ -0,0 +1,201 @@ +**_THIS REPOSITORY AND PACKAGE WILL BE DEPRECATED IN JULY 2024. RELEVANT FUNCTIONALITIES HAVE BEEN MOVED TO [GOOGLEAPIS/GAXIOS](https://github.com/googleapis/gaxios)_** + +|![retry-request](logo.png) +|:-: +|Retry a [request][request] with built-in [exponential backoff](https://developers.google.com/analytics/devguides/reporting/core/v3/coreErrors#backoff). + +```sh +$ npm install --save teeny-request +$ npm install --save retry-request +``` + +```js +var request = require('retry-request', { + request: require('teeny-request'), +}); +``` + +It should work the same as `request` and `teeny-request` in both callback mode and stream mode. + +Note: This module only works when used as a readable stream, i.e. POST requests aren't supported ([#3](https://github.com/googleapis/retry-request/issues/3)). + +## Do I need to install `request`? + +Yes! You must independently install `teeny-request` OR `request` (_deprecated_) and provide it to this library: + +```js +var request = require('retry-request', { + request: require('teeny-request'), +}); +``` + +#### Callback + +`urlThatReturns503` will be requested 3 total times before giving up and executing the callback. + +```js +request(urlThatReturns503, function (err, resp, body) {}); +``` + +#### Stream + +`urlThatReturns503` will be requested 3 total times before giving up and emitting the `response` and `complete` event as usual. + +```js +request(urlThatReturns503) + .on('error', function () {}) + .on('response', function () {}) + .on('complete', function () {}); +``` + +## Can I monitor what retry-request is doing internally? + +Yes! To enable the debug mode, set the environment variable `DEBUG` to _retry-request_. + +(Thanks for the implementation, @yihaozhadan!) + +## request(requestOptions, [opts], [cb]) + +### requestOptions + +Passed directly to `request` or `teeny-request`. See the list of options supported: + +- https://github.com/request/request/#requestoptions-callback +- https://github.com/googleapis/teeny-request#teenyrequestoptions-callback + +### opts _(optional)_ + +#### `opts.noResponseRetries` + +Type: `Number` + +Default: `2` + +The number of times to retry after a response fails to come through, such as a DNS resolution error or a socket hangup. + +```js +var opts = { + noResponseRetries: 0, +}; + +request(url, opts, function (err, resp, body) { + // url was requested 1 time before giving up and + // executing this callback. +}); +``` + +#### `opts.objectMode` + +Type: `Boolean` + +Default: `false` + +Set to `true` if your custom `opts.request` function returns a stream in object mode. + +#### `opts.retries` + +Type: `Number` + +Default: `2` + +```js +var opts = { + retries: 4, +}; + +request(urlThatReturns503, opts, function (err, resp, body) { + // urlThatReturns503 was requested a total of 5 times + // before giving up and executing this callback. +}); +``` + +#### `opts.currentRetryAttempt` + +Type: `Number` + +Default: `0` + +```js +var opts = { + currentRetryAttempt: 1, +}; + +request(urlThatReturns503, opts, function (err, resp, body) { + // urlThatReturns503 was requested as if it already failed once. +}); +``` + +#### `opts.shouldRetryFn` + +Type: `Function` + +Default: Returns `true` if [http.incomingMessage](https://nodejs.org/api/http.html#http_http_incomingmessage).statusCode is < 200 or >= 400. + +```js +var opts = { + shouldRetryFn: function (incomingHttpMessage) { + return incomingHttpMessage.statusMessage !== 'OK'; + }, +}; + +request(urlThatReturnsNonOKStatusMessage, opts, function (err, resp, body) { + // urlThatReturnsNonOKStatusMessage was requested a + // total of 3 times, each time using `opts.shouldRetryFn` + // to decide if it should continue before giving up and + // executing this callback. +}); +``` + +#### `opts.request` + +Type: `Function` + +If we not provided we will throw an error advising you to provide it. + +_NOTE: If you override the request function, and it returns a stream in object mode, be sure to set `opts.objectMode` to `true`._ + +```js +var originalRequest = require('teeny-request').defaults({ + pool: { + maxSockets: Infinity, + }, +}); + +var opts = { + request: originalRequest, +}; + +request(urlThatReturns503, opts, function (err, resp, body) { + // Your provided `originalRequest` instance was used. +}); +``` + +#### `opts.maxRetryDelay` + +Type: `Number` + +Default: `64` + +The maximum time to delay in seconds. If retryDelayMultiplier results in a delay greater than maxRetryDelay, retries should delay by maxRetryDelay seconds instead. + +#### `opts.retryDelayMultiplier` + +Type: `Number` + +Default: `2` + +The multiplier by which to increase the delay time between the completion of failed requests, and the initiation of the subsequent retrying request. + +#### `opts.totalTimeout` + +Type: `Number` + +Default: `600` + +The length of time to keep retrying in seconds. The last sleep period will be shortened as necessary, so that the last retry runs at deadline (and not considerably beyond it). The total time starting from when the initial request is sent, after which an error will be returned, regardless of the retrying attempts made meanwhile. + +### cb _(optional)_ + +Passed directly to `request`. See the callback section: https://github.com/request/request/#requestoptions-callback. + +[request]: https://github.com/request/request diff --git a/packages/retry-request/test.js b/packages/retry-request/test.js new file mode 100644 index 000000000..74eb4f80b --- /dev/null +++ b/packages/retry-request/test.js @@ -0,0 +1,554 @@ +'use strict'; + +const assert = require('assert'); +const async = require('async'); +const range = require('lodash.range'); +const {describe, it, beforeEach} = require('mocha'); +const {PassThrough} = require('stream'); +const {teenyRequest} = require('teeny-request'); + +const retryRequest = require('./index.js'); + +retryRequest.defaults.request = teenyRequest.defaults(); + +describe('retry-request', () => { + const URI_404 = 'http://google.com/theblahstore'; + const URI_200 = 'http://google.com/'; + const URI_NON_EXISTENT = 'http://theblahstore'; + + describe('streams', () => { + it('works with defaults in a stream', done => { + let responsesEmitted = 0; + + retryRequest(URI_404) + .on('error', done) + .on('response', () => { + responsesEmitted++; + }) + .on('complete', () => { + assert.strictEqual(responsesEmitted, 1); + done(); + }); + }); + + it('allows object mode', () => { + const retryStream = retryRequest(URI_404, {objectMode: true}); + assert.strictEqual(retryStream._readableState.objectMode, true); + }); + + it('emits an error', done => { + retryRequest(URI_NON_EXISTENT).on('error', () => { + done(); + }); + }); + + it('emits a `request` event on each request', done => { + let requestsMade = 0; + let requestsEmitted = 0; + + const opts = { + shouldRetryFn: function () { + return requestsMade < 3; + }, + request: function () { + const fakeRequestStream = new PassThrough(); + + requestsMade++; + + setImmediate(() => { + fakeRequestStream.emit('response', {statusCode: 200}); + + if (requestsMade === 3) { + setImmediate(() => { + fakeRequestStream.emit('complete'); + }); + } + }); + + return fakeRequestStream; + }, + }; + + retryRequest(URI_404, opts) + .on('request', () => { + requestsEmitted++; + }) + .on('error', done) + .on('complete', () => { + assert.strictEqual(requestsEmitted, 3); + done(); + }); + }); + + it('exposes an `abort` function to match request', done => { + const retryStream = retryRequest(URI_NON_EXISTENT); + + retryStream.on('error', () => { + assert.equal(typeof retryStream.abort, 'function'); + done(); + }); + }); + + it('works on the last attempt', done => { + let numAborts = 0; + let numAttempts = 0; + + const opts = { + request: function () { + numAttempts++; + + const fakeRequestStream = new PassThrough(); + fakeRequestStream.abort = function () { + numAborts++; + }; + + const shouldReturnError = numAttempts < 3; + const response = shouldReturnError + ? {statusCode: 503} + : {statusCode: 200}; + + setImmediate(() => { + fakeRequestStream.emit('response', response); + + if (shouldReturnError) { + return; + } + + setImmediate(() => { + fakeRequestStream.emit('complete', numAttempts); + }); + }); + + return fakeRequestStream; + }, + }; + + retryRequest(URI_404, opts) + .on('error', done) + .on('complete', numAttempts => { + assert.strictEqual(numAborts, 2); + assert.deepEqual(numAttempts, 3); + done(); + }); + }); + + it('never succeeds', done => { + let numAborts = 0; + let numAttempts = 0; + + const opts = { + request: function () { + numAttempts++; + + const fakeRequestStream = new PassThrough(); + fakeRequestStream.abort = function () { + numAborts++; + }; + + const response = {statusCode: 503}; + setImmediate(() => { + fakeRequestStream.emit('response', response); + }); + + return fakeRequestStream; + }, + }; + + retryRequest(URI_404, opts) + .on('response', () => { + assert.strictEqual(numAborts, 2); + assert.strictEqual(numAttempts, 3); + done(); + }) + .on('error', done); + }); + + it('forwards a request error', done => { + const error = new Error('Error.'); + + const opts = { + request: function () { + const fakeRequestStream = new PassThrough(); + + setImmediate(() => { + fakeRequestStream.emit('response', { + statusCode: 200, + }); + + setImmediate(() => { + fakeRequestStream.destroy(error); + }); + }); + + return fakeRequestStream; + }, + }; + + retryRequest(URI_200, opts).on('error', err => { + assert.strictEqual(err, error); + done(); + }); + }); + }); + + describe('callbacks', () => { + it('works with defaults with a callback', done => { + retryRequest(URI_404, () => { + done(); + }); + }); + + it('exposes an `abort` function', done => { + const opts = { + request: function () { + return { + abort: done, + }; + }, + }; + + const request = retryRequest(URI_200, opts, assert.ifError); + request.abort(); + }); + + it('returns an error', done => { + retryRequest(URI_NON_EXISTENT, err => { + assert.equal(typeof err, 'object'); + done(); + }); + }); + }); + + describe('overriding', () => { + it('should ignore undefined options', done => { + let numAttempts = 0; + const error = new Error('ENOTFOUND'); + + const opts = { + noResponseRetries: undefined, + request: function (_, callback) { + numAttempts++; + callback(error); + }, + }; + + retryRequest(URI_NON_EXISTENT, opts, err => { + assert.strictEqual(numAttempts, 3); + assert.strictEqual(err, error); + done(); + }); + }); + + it('should allow overriding retries', done => { + const opts = {retries: 0}; + + retryRequest(URI_404, opts, () => { + done(); + }); + }); + + it('should use default noResponseRetries', done => { + let numAttempts = 0; + const error = new Error('ENOTFOUND'); + + const opts = { + request: function (_, callback) { + numAttempts++; + callback(error); + }, + }; + + retryRequest(URI_NON_EXISTENT, opts, err => { + assert.strictEqual(numAttempts, 3); + assert.strictEqual(err, error); + done(); + }); + }); + + it('should allow overriding noResponseRetries', done => { + let numAttempts = 0; + const error = new Error('ENOTFOUND'); + + const opts = { + noResponseRetries: 0, + request: function (_, callback) { + numAttempts++; + callback(error); + }, + }; + + retryRequest(URI_NON_EXISTENT, opts, err => { + assert.strictEqual(numAttempts, 1); + assert.strictEqual(err, error); + done(); + }); + }); + + it('should allow overriding currentRetryAttempt', done => { + let numAttempts = 0; + const opts = { + currentRetryAttempt: 1, + request: function (_, responseHandler) { + numAttempts++; + responseHandler(null, {statusCode: 500}); + }, + }; + + retryRequest(URI_404, opts, () => { + assert.strictEqual(numAttempts, 1); + done(); + }); + }); + + it('should allow overriding shouldRetryFn', done => { + let shouldRetryFnCalled = false; + + const opts = { + retries: 1, // so that our retry function is only called once + + shouldRetryFn: function () { + shouldRetryFnCalled = true; + return true; + }, + }; + + retryRequest(URI_404, opts, () => { + assert.strictEqual(shouldRetryFnCalled, true); + done(); + }); + }); + + it('should allow overriding request', done => { + const opts = { + request: function () { + done(); + }, + }; + + retryRequest(URI_200, opts, () => {}); + }); + }); + + describe('shouldRetryFn', () => { + const URI = 'http://'; + + function assertRetried(statusCode, callback) { + let initialRequestMade = false; + + retryRequest( + URI, + { + request: function (_, responseHandler) { + if (initialRequestMade) { + // This is a retry attempt. "Test passed" + callback(); + return; + } + + initialRequestMade = true; + responseHandler(null, {statusCode: statusCode}); + }, + }, + assert.ifError, + ); + } + + function assertNotRetried(statusCode, callback) { + let initialRequestMade = false; + let requestWasRetried = false; + + retryRequest( + URI, + { + request: function (_, responseHandler) { + requestWasRetried = initialRequestMade; + initialRequestMade = true; + responseHandler(null, {statusCode: statusCode}); + }, + }, + err => { + if (err) { + callback(err); + return; + } + + if (requestWasRetried) { + callback(new Error('Request was retried')); + return; + } + + callback(); + }, + ); + } + + it('should retry a 1xx code', done => { + async.each(range(100, 199), assertRetried, done); + }); + + it('should not retry a 2xx code', done => { + async.each(range(200, 299), assertNotRetried, done); + }); + + it('should not retry a 3xx code', done => { + async.each(range(300, 399), assertNotRetried, done); + }); + + it('should not retry a 4xx code', done => { + const statusCodes = range(400, 428).concat(range(430, 499)); + + async.each(statusCodes, assertNotRetried, done); + }); + + it('should retry a 429 code', done => { + assertRetried(429, done); + }); + + it('should retry a 5xx code', done => { + async.each(range(500, 599), assertRetried, done); + }); + }); + + it('should not do any retries if unnecessary', done => { + let shouldRetryFnTimesCalled = 0; + + const opts = { + shouldRetryFn: function () { + shouldRetryFnTimesCalled++; + return false; + }, + }; + + retryRequest(URI_200, opts, () => { + assert.strictEqual(shouldRetryFnTimesCalled, 1); + done(); + }); + }); + + it('has an initial delay when currentRetryAttempt > 0', done => { + const startTime = new Date(); + + const opts = { + currentRetryAttempt: 1, + request: function (_, responseHandler) { + responseHandler(null, {statusCode: 200}); + }, + }; + + retryRequest(URI_200, opts, () => { + const totalTime = new Date() - startTime; + console.log(`TOTAL TIME: ${totalTime}`); + assert(totalTime >= 2000 && totalTime < 3500); + done(); + }); + }); +}); + +describe('getNextRetryDelay', () => { + const maxRetryDelay = 64; + const retryDelayMultiplier = 2; + let timeOfFirstRequest; + const totalTimeout = 64; + + function secondsToMs(seconds) { + return seconds * 1000; + } + + beforeEach(() => { + timeOfFirstRequest = Date.now(); + }); + + it('should return exponential retry delay', () => { + [1, 2, 3, 4, 5].forEach(assertTime); + + function assertTime(retryNumber) { + const min = Math.pow(2, retryNumber) * secondsToMs(1); + const max = Math.pow(2, retryNumber) * secondsToMs(1) + secondsToMs(1); + + const delay = retryRequest.getNextRetryDelay({ + maxRetryDelay, + retryDelayMultiplier, + retryNumber, + timeOfFirstRequest, + totalTimeout, + }); + + assert(delay >= min && delay <= max); + } + }); + + it('should allow overriding the multiplier', () => { + [1, 2, 3, 4, 5].forEach(assertTime); + + function assertTime(multiplier) { + const min = Math.pow(multiplier, 1) * secondsToMs(1); + const max = Math.pow(multiplier, 1) * secondsToMs(1) + secondsToMs(1); + + const delay = retryRequest.getNextRetryDelay({ + maxRetryDelay, + retryDelayMultiplier: multiplier, + retryNumber: 1, + timeOfFirstRequest, + totalTimeout, + }); + + assert(delay >= min && delay <= max); + } + }); + + it('should honor total timeout setting', () => { + // This test passes settings to calculate an enormous retry delay, if it + // weren't for the timeout restrictions imposed by `totalTimeout`. + // So, even though this is pretending to be the 10th retry, and our + // `maxRetryDelay` is huge, the 60 second max timeout we have for all + // requests to complete by is honored. + // We tell the function that we have already been trying this request for + // 30 seconds, and we will only wait a maximum of 60 seconds. Therefore, we + // should end up with a retry delay of around 30 seconds. + const retryDelay = retryRequest.getNextRetryDelay({ + // Allow 60 seconds maximum delay, + timeOfFirstRequest: Date.now() - secondsToMs(30), // 30 seconds ago. + totalTimeout: 60, + + // Inflating these numbers to be sure the smaller timeout is chosen: + maxRetryDelay: 1e9, + retryDelayMultiplier: 10, + retryNumber: 10, + }); + + const min = retryDelay - 10; + const max = retryDelay + 10; + assert(retryDelay >= min && retryDelay <= max); + }); + + it('should return maxRetryDelay if calculated retry would be too high', () => { + const delayWithoutLowMaxRetryDelay = retryRequest.getNextRetryDelay({ + maxRetryDelay, + retryDelayMultiplier, + retryNumber: 100, + timeOfFirstRequest, + totalTimeout, + }); + + const maxRetryDelayMs = secondsToMs(maxRetryDelay); + const min = maxRetryDelayMs - 10; + const max = maxRetryDelayMs + 10; + assert( + delayWithoutLowMaxRetryDelay >= min && + delayWithoutLowMaxRetryDelay <= max, + ); + + const lowMaxRetryDelay = 1; + const delayWithLowMaxRetryDelay = retryRequest.getNextRetryDelay({ + maxRetryDelay: lowMaxRetryDelay, + retryDelayMultiplier, + retryNumber: 100, + timeOfFirstRequest, + totalTimeout, + }); + assert.strictEqual( + delayWithLowMaxRetryDelay, + secondsToMs(lowMaxRetryDelay), + ); + }); +}); diff --git a/packages/retry-request/tsconfig.json b/packages/retry-request/tsconfig.json new file mode 100644 index 000000000..364945484 --- /dev/null +++ b/packages/retry-request/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", +} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index 02f5a790a..e5024d3a2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,12 +7,13 @@ "dev-packages/logging-utils": {}, "dev-packages/pack-n-play": {}, "generator/gapic-generator-typescript": {}, - "packages/google-auth-library-nodejs": {}, "packages/gax": {}, "packages/gaxios": {}, "packages/gcp-metadata": {}, + "packages/google-auth-library-nodejs": {}, "packages/nodejs-googleapis-common": {}, "packages/proto3-json-serializer-nodejs": {}, + "packages/retry-request": {}, "packages/tools": {} }, "plugins": [