From 3bb9ef4cc935e0fa5dbf825b21d833944ac58a45 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 26 Jun 2020 14:17:50 +0000 Subject: [PATCH] Switch from using custom HTTP client caching to `make-fetch-happen`. This is a similar treatment as to what was applied to `@apollo/gateway` in https://github.com/apollographql/apollo-server/pull/3783. This replaces a local HTTP cache implementation which continues to grow in complexity with an off-the-shelf Fetch API client with many valuable bells and whistles. In particular, it uses `make-fetch-happen`, which is a relatively full-featured Node.js based implementation which is used by `npm` itself and leverages `minipass-fetch` under the hood. This leverages the same cache implementation used in `@apollo/gateway` and duplicates that cache logic. It's entirely possible we would be well-served to use the `cache.js` example that's included in the source of `make-fetch-happen` and leverages `cacache`, but as of this message, it doesn't include the TypeScript types and this implementation seems to work. This does remove a test which was previously valuable but should no longer be necessary. Specifically, sine we now have an HTTP implementation that handles caching and retries itself, we do handle intermediary retries within that layer. We still test the polling (i.e, the literal existence of an interval which re-fires) in other tests, but it was too tricky to try to re-jigger this test with the abstraction. I think this is a good thing to not need to worry about, but we can consider re-adding it in the event of regressions. Ref: https://npm.im/make-fetch-happen Ref: https://npm.im/minipass-fetch Ref: https://npm.im/cacache Ref: https://github.com/npm/make-fetch-happen/blob/b04c4c16/cache.js --- package-lock.json | 341 +++++++++++++++++- .../package.json | 3 +- .../ApolloServerPluginOperationRegistry.ts | 3 + .../src/__tests__/agent.test.ts | 54 --- .../src/agent.ts | 48 ++- .../src/cache.ts | 55 +++ .../src/fetchIfNoneMatch.ts | 21 -- 7 files changed, 434 insertions(+), 91 deletions(-) create mode 100644 packages/apollo-server-plugin-operation-registry/src/cache.ts delete mode 100644 packages/apollo-server-plugin-operation-registry/src/fetchIfNoneMatch.ts diff --git a/package-lock.json b/package-lock.json index 3af192b35c4..2625d979216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4421,6 +4421,21 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "@octokit/auth-token": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", @@ -5553,6 +5568,15 @@ "humanize-ms": "^1.2.1" } }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -6071,12 +6095,13 @@ "requires": { "apollo-graphql": "0.4.5", "apollo-server-caching": "file:packages/apollo-server-caching", + "apollo-server-env": "file:packages/apollo-server-env", "apollo-server-errors": "file:packages/apollo-server-errors", "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "fast-json-stable-stringify": "^2.0.0", "loglevel": "^1.6.1", "loglevel-debug": "^0.0.1", - "node-fetch": "^2.3.0" + "make-fetch-happen": "^8.0.7" }, "dependencies": { "@types/node-fetch": { @@ -6088,6 +6113,24 @@ "form-data": "^3.0.0" } }, + "agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.3.tgz", + "integrity": "sha512-wn8fw19xKZwdGPO47jivonaHRTd+nGOMP1z11sgGeQzDy2xd5FG0R67dIMcKHDE2cJ5y+YXV30XVGUBPRSY7Hg==", + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, "apollo-env": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", @@ -6108,6 +6151,35 @@ "lodash.sortby": "^4.7.0" } }, + "cacache": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.4.tgz", + "integrity": "sha512-YlnKQqTbD/6iyoJvEY3KJftjrdBYroCbxxYXzhOzsFLWlp6KX4BOlEf4mTx0cMUfVaTS3ENL2QtDWeRYoGLkkw==", + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6116,6 +6188,14 @@ "delayed-stream": "~1.0.0" } }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, "form-data": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", @@ -6125,6 +6205,152 @@ "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "make-fetch-happen": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.7.tgz", + "integrity": "sha512-rkDA4c1nMXVqLkfOaM5RK2dxkUndjLOCrPycTDZgbkFDzhmaCO3P1dmCW//yt1I/G1EcedJqMsSjWkV79Hh4hQ==", + "requires": { + "agentkeepalive": "^4.1.0", + "cacache": "^15.0.0", + "http-cache-semantics": "^4.0.4", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^5.1.1", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.1.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^5.0.0", + "ssri": "^8.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "socks-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz", + "integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==", + "requires": { + "agent-base": "6", + "debug": "4", + "socks": "^2.3.3" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + } + }, + "tar": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", + "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -10810,8 +11036,7 @@ "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, "infer-owner": { "version": "1.0.4", @@ -11134,6 +11359,11 @@ "is-extglob": "^2.1.1" } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -16237,6 +16467,111 @@ "yallist": "^3.0.0" } }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-fetch": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.2.1.tgz", + "integrity": "sha512-ssHt0dkljEDaKmTgQ04DQgx2ag6G2gMPxA5hpcsoeTbfDgRf2fC2gNSRc6kISjD7ckCpHwwQvXxuTBK8402fXg==", + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-pipeline": "^1.2.2", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "minipass-pipeline": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", + "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", diff --git a/packages/apollo-server-plugin-operation-registry/package.json b/packages/apollo-server-plugin-operation-registry/package.json index fa4e088107d..59ff64dfbf4 100644 --- a/packages/apollo-server-plugin-operation-registry/package.json +++ b/packages/apollo-server-plugin-operation-registry/package.json @@ -18,12 +18,13 @@ "dependencies": { "apollo-graphql": "0.4.5", "apollo-server-caching": "file:../apollo-server-caching", + "apollo-server-env": "file:../apollo-server-env", "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "fast-json-stable-stringify": "^2.0.0", "loglevel": "^1.6.1", "loglevel-debug": "^0.0.1", - "node-fetch": "^2.3.0" + "make-fetch-happen": "^8.0.7" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" diff --git a/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts b/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts index 5325f46cff8..a9debf3be62 100644 --- a/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts +++ b/packages/apollo-server-plugin-operation-registry/src/ApolloServerPluginOperationRegistry.ts @@ -23,6 +23,7 @@ import { GraphQLSchema } from 'graphql/type'; import { InMemoryLRUCache } from 'apollo-server-caching'; import loglevel from 'loglevel'; import loglevelDebug from 'loglevel-debug'; +import { fetch } from "apollo-server-env"; type ForbidUnregisteredOperationsPredicate = ( requestContext: GraphQLRequestContext, @@ -45,6 +46,7 @@ export interface OperationManifest { export interface Options { debug?: boolean; + fetcher?: typeof fetch; forbidUnregisteredOperations?: | boolean | ForbidUnregisteredOperationsPredicate; @@ -136,6 +138,7 @@ for observability purposes, but all operations will be permitted.`, engine, store, logger, + fetcher: options.fetcher, }); await agent.start(); diff --git a/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts b/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts index 3f48862e29e..2f948a55bd9 100644 --- a/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts +++ b/packages/apollo-server-plugin-operation-registry/src/__tests__/agent.test.ts @@ -283,60 +283,6 @@ describe('Agent', () => { expect(storeDeleteSpy).toBeCalledTimes(0); }); - it('continues polling even after initial failure', async () => { - nockStorageSecret(genericServiceID, genericApiKeyHash); - nockStorageSecretOperationManifest( - genericServiceID, - genericStorageSecret, - 500, - ); - const store = defaultStore(); - const storeSetSpy = jest.spyOn(store, 'set'); - const storeDeleteSpy = jest.spyOn(store, 'delete'); - const agent = createAgent({ store }); - jest.useFakeTimers(); - await agent.start(); - - expect(storeSetSpy).toBeCalledTimes(0); - expect(storeDeleteSpy).toBeCalledTimes(0); - - // Only the initial start-up check should have happened by now. - expect(agent._timesChecked).toBe(1); - - // If it's one millisecond short of our next poll interval, nothing - // should have changed yet. - jest.advanceTimersByTime(defaultTestAgentPollSeconds * 1000 - 1); - - // Still only one check. - expect(agent._timesChecked).toBe(1); - expect(storeSetSpy).toBeCalledTimes(0); - - // Now, we'll expect another GOOD request to fulfill, so we'll nock it. - nockStorageSecret(genericServiceID, genericApiKeyHash); - nockGoodManifestsUnderStorageSecret( - genericServiceID, - genericStorageSecret, - [ - sampleManifestRecords.a, - sampleManifestRecords.b, - sampleManifestRecords.c, - ], - ); - - // If we move forward the last remaining millisecond, we should trigger - // and end up with a successful sync. - jest.advanceTimersByTime(1); - - // While that timer will fire, it will do work async, and we need to - // wait on that work itself. - await agent.requestPending(); - - // Now the times checked should have gone up. - expect(agent._timesChecked).toBe(2); - // And store should have been called with operations ABC - expect(storeSetSpy).toBeCalledTimes(3); - }); - it('purges operations which are removed from the manifest', async () => { const store = defaultStore(); const storeSetSpy = jest.spyOn(store, 'set'); diff --git a/packages/apollo-server-plugin-operation-registry/src/agent.ts b/packages/apollo-server-plugin-operation-registry/src/agent.ts index 5dbb4ea1561..01fc38ceffa 100644 --- a/packages/apollo-server-plugin-operation-registry/src/agent.ts +++ b/packages/apollo-server-plugin-operation-registry/src/agent.ts @@ -8,18 +8,20 @@ import { } from './common'; import loglevel from 'loglevel'; +import fetcher from 'make-fetch-happen'; +import { HttpRequestCache } from './cache'; -import { Response } from 'node-fetch'; import { InMemoryLRUCache } from 'apollo-server-caching'; -import { fetchIfNoneMatch } from './fetchIfNoneMatch'; import { OperationManifest } from "./ApolloServerPluginOperationRegistry"; import { Logger } from "apollo-server-types"; +import { Response, fetch } from "apollo-server-env"; const DEFAULT_POLL_SECONDS: number = 30; const SYNC_WARN_TIME_SECONDS: number = 60; export interface AgentOptions { logger?: Logger; + fetcher?: typeof fetch; pollSeconds?: number; schemaHash: string; engine: any; @@ -32,6 +34,7 @@ type SignatureStore = Set; const callToAction = `Ensure this server's schema has been published with 'apollo service:push' and that operations have been registered with 'apollo client:push'.`; export default class Agent { + private fetcher: typeof fetch; private timer?: NodeJS.Timer; private logger: Logger; private hashedServiceId?: string; @@ -50,6 +53,8 @@ export default class Agent { this.logger = this.options.logger || loglevel.getLogger(pluginName); + this.fetcher = this.options.fetcher || getDefaultGcsFetcher(); + if (!this.options.schemaHash) { throw new Error('`schemaHash` must be passed to the Agent.'); } @@ -146,11 +151,7 @@ export default class Agent { this.options.engine.apiKeyHash, ); - const response = await fetchIfNoneMatch(storageSecretUrl, { - method: 'GET', - // More than three times our polling interval should be long enough to wait. - timeout: this.pollSeconds() * 3 /* times */ * 1000 /* ms */, - }); + const response = await this.fetcher(storageSecretUrl, this.fetchOptions); if (response.status === 304) { this.logger.debug( @@ -192,7 +193,7 @@ export default class Agent { this.options.schemaHash, ); this.logger.debug(`Checking for manifest changes at ${legacyManifestUrl}`); - return fetchIfNoneMatch(legacyManifestUrl, this.fetchOptions); + return this.fetcher(legacyManifestUrl, this.fetchOptions); } private async fetchManifest(): Promise { @@ -213,10 +214,8 @@ export default class Agent { this.logger.debug( `Checking for manifest changes at ${storageSecretManifestUrl}`, ); - const response = await fetchIfNoneMatch( - storageSecretManifestUrl, - this.fetchOptions, - ); + const response = + await this.fetcher(storageSecretManifestUrl, this.fetchOptions); if (response.status === 404 || response.status === 403) { this.logger.warn( @@ -341,3 +340,28 @@ export default class Agent { this.lastOperationSignatures = replacementSignatures; } } + +const GCS_RETRY_COUNT = 5; + +function getDefaultGcsFetcher() { + return fetcher.defaults({ + cacheManager: new HttpRequestCache(), + // All headers should be lower-cased here, as `make-fetch-happen` + // treats differently cased headers as unique (unlike the `Headers` object). + // @see: https://git.io/JvRUa + headers: { + 'user-agent': [ + require('../package.json').name, + require('../package.json').version + ].join('/'), + }, + retry: { + retries: GCS_RETRY_COUNT, + // The default factor: expected attempts at 0, 1, 3, 7, 15, and 31 seconds elapsed + factor: 2, + // 1 second + minTimeout: 1000, + randomize: true, + }, + }); +} diff --git a/packages/apollo-server-plugin-operation-registry/src/cache.ts b/packages/apollo-server-plugin-operation-registry/src/cache.ts new file mode 100644 index 00000000000..177aa9bbe18 --- /dev/null +++ b/packages/apollo-server-plugin-operation-registry/src/cache.ts @@ -0,0 +1,55 @@ +import { CacheManager } from 'make-fetch-happen'; +import { Request, Response, Headers } from 'apollo-server-env'; +import { InMemoryLRUCache } from 'apollo-server-caching'; + +const MAX_SIZE = 5 * 1024 * 1024; // 5MB + +function cacheKey(request: Request) { + return `op-reg:request-cache:${request.method}:${request.url}`; +} + +interface CachedRequest { + body: string; + status: number; + statusText: string; + headers: Headers; +} + +export class HttpRequestCache implements CacheManager { + constructor( + public cache: InMemoryLRUCache = new InMemoryLRUCache({ + maxSize: MAX_SIZE, + }), + ) {} + + // Return true if entry exists, else false + async delete(request: Request) { + const key = cacheKey(request); + const entry = await this.cache.get(key); + await this.cache.delete(key); + return Boolean(entry); + } + + async put(request: Request, response: Response) { + let body = await response.text(); + + this.cache.set(cacheKey(request), { + body, + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + + return new Response(body, response); + } + + async match(request: Request) { + return this.cache.get(cacheKey(request)).then(response => { + if (response) { + const { body, ...requestInit } = response; + return new Response(body, requestInit); + } + return; + }); + } +} diff --git a/packages/apollo-server-plugin-operation-registry/src/fetchIfNoneMatch.ts b/packages/apollo-server-plugin-operation-registry/src/fetchIfNoneMatch.ts deleted file mode 100644 index 3d232312973..00000000000 --- a/packages/apollo-server-plugin-operation-registry/src/fetchIfNoneMatch.ts +++ /dev/null @@ -1,21 +0,0 @@ -import fetch, { Response, RequestInit } from 'node-fetch'; - -const urlEtagMap: { [url: string]: string | null } = {}; - -export async function fetchIfNoneMatch( - url: string, - fetchOptions?: RequestInit, -): Promise { - const previousEtag = urlEtagMap[url]; - - const response = await fetch(url, { - ...fetchOptions, - headers: { - ...(fetchOptions && fetchOptions.headers), - ...(previousEtag && { 'If-None-Match': previousEtag }), - }, - }); - - urlEtagMap[url] = response.headers.get('etag'); - return response; -}