From 67d9036bdd7a8e0122c6c77a68d48a28e0fad904 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Tue, 7 Jun 2022 15:35:38 -0700 Subject: [PATCH] Implement simple `UnboundedCache` (#6535) This introduces a simple Map-backed, unbounded, in-memory cache which implements TTLs. This lets us remove the dependency on keyv and @apollo/utils.keyvadapter for a thing that we're going to actively tell people not to use anyway. --- CHANGELOG.md | 1 + package-lock.json | 123 ------------------ packages/apollo-server-core/package.json | 2 - .../apollo-server-core/src/ApolloServer.ts | 5 +- .../src/__tests__/UnboundedCache.test.ts | 35 +++++ .../src/__tests__/documentStore.test.ts | 4 +- .../src/utils/UnboundedCache.ts | 35 +++++ 7 files changed, 75 insertions(+), 130 deletions(-) create mode 100644 packages/apollo-server-core/src/__tests__/UnboundedCache.test.ts create mode 100644 packages/apollo-server-core/src/utils/UnboundedCache.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e8be3244870..e1dd0811d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The version headers in this history reflect the versions of Apollo Server itself ## vNEXT - Remove internal dependency on `apollo-server-caching`, switch over to `@apollo/utils.keyvaluecache`. This PR specifically also introduces Keyv as an unbounded cache solution, but will replace with our own simple implementation in a follow-up PR targeting this minor version release. [PR #6522](https://github.com/apollographql/apollo-server/pull/6522) +- Remove dependency on `keyv`/`@apollo/utils.keyvadapter` in favor of a simple `Map`-backed cache which implements TTL [PR #6535](https://github.com/apollographql/apollo-server/pull/6535) ## v3.8.2 diff --git a/package-lock.json b/package-lock.json index 0b1e6b826eb..341316ef204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,29 +209,6 @@ "graphql": "14.x || 15.x || 16.x" } }, - "node_modules/@apollo/utils.keyvadapter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@apollo/utils.keyvadapter/-/utils.keyvadapter-1.0.1.tgz", - "integrity": "sha512-o8u9w33txkxRzEAdvkZ3myGp0D9ShZqJZOB2bCQMcKs+IlXyedxAV1dlPirijFW0UiLYJ6V82vJNacrTejT38A==", - "dependencies": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "keyv": "^4.2.8" - } - }, - "node_modules/@apollo/utils.keyvadapter/node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/@apollo/utils.keyvadapter/node_modules/keyv": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", - "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", - "dependencies": { - "compress-brotli": "^1.3.8", - "json-buffer": "3.0.1" - } - }, "node_modules/@apollo/utils.keyvaluecache": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", @@ -6727,11 +6704,6 @@ "integrity": "sha512-KbeHS/Y4R+k+5sWXEYzAZKuB1yQlZtEghuhRxrVRLaqhtoG5+26JwQsa4HyS3AWX8v1Uwukma5HheduUDskasA==", "dev": true }, - "node_modules/@types/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==" - }, "node_modules/@types/json-stable-stringify": { "version": "1.0.33", "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.33.tgz", @@ -8798,23 +8770,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compress-brotli": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", - "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", - "dependencies": { - "@types/json-buffer": "~3.0.0", - "json-buffer": "~3.0.1" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/compress-brotli/node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -21455,7 +21410,6 @@ "version": "3.8.2", "license": "MIT", "dependencies": { - "@apollo/utils.keyvadapter": "^1.0.1", "@apollo/utils.keyvaluecache": "^1.0.1", "@apollo/utils.logger": "^1.0.0", "@apollo/utils.usagereporting": "^1.0.0", @@ -21473,7 +21427,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "keyv": "^4.3.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", @@ -21487,20 +21440,6 @@ "graphql": "^15.3.0 || ^16.0.0" } }, - "packages/apollo-server-core/node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "packages/apollo-server-core/node_modules/keyv": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", - "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", - "dependencies": { - "compress-brotli": "^1.3.8", - "json-buffer": "3.0.1" - } - }, "packages/apollo-server-core/node_modules/uuid": { "version": "8.3.2", "license": "MIT", @@ -21814,31 +21753,6 @@ "integrity": "sha512-R2iZgXB+vTEx+B+N2mME0SP61+iEt+5fM26bYBvubyZflKed1VH2YiPLIl44nrahIph5JsTIvi/CeUsveJRZkg==", "requires": {} }, - "@apollo/utils.keyvadapter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@apollo/utils.keyvadapter/-/utils.keyvadapter-1.0.1.tgz", - "integrity": "sha512-o8u9w33txkxRzEAdvkZ3myGp0D9ShZqJZOB2bCQMcKs+IlXyedxAV1dlPirijFW0UiLYJ6V82vJNacrTejT38A==", - "requires": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "keyv": "^4.2.8" - }, - "dependencies": { - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "keyv": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", - "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", - "requires": { - "compress-brotli": "^1.3.8", - "json-buffer": "3.0.1" - } - } - } - }, "@apollo/utils.keyvaluecache": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-1.0.1.tgz", @@ -26848,11 +26762,6 @@ "integrity": "sha512-KbeHS/Y4R+k+5sWXEYzAZKuB1yQlZtEghuhRxrVRLaqhtoG5+26JwQsa4HyS3AWX8v1Uwukma5HheduUDskasA==", "dev": true }, - "@types/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==" - }, "@types/json-stable-stringify": { "version": "1.0.33", "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.33.tgz", @@ -27437,7 +27346,6 @@ "apollo-server-core": { "version": "file:packages/apollo-server-core", "requires": { - "@apollo/utils.keyvadapter": "^1.0.1", "@apollo/utils.keyvaluecache": "^1.0.1", "@apollo/utils.logger": "^1.0.0", "@apollo/utils.usagereporting": "^1.0.0", @@ -27455,7 +27363,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "keyv": "^4.3.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", @@ -27463,20 +27370,6 @@ "whatwg-mimetype": "^3.0.0" }, "dependencies": { - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "keyv": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", - "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", - "requires": { - "compress-brotli": "^1.3.8", - "json-buffer": "3.0.1" - } - }, "uuid": { "version": "8.3.2" }, @@ -28643,22 +28536,6 @@ "version": "1.3.0", "dev": true }, - "compress-brotli": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", - "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", - "requires": { - "@types/json-buffer": "~3.0.0", - "json-buffer": "~3.0.1" - }, - "dependencies": { - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - } - } - }, "concat-map": { "version": "0.0.1", "dev": true diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index c3704e5c397..0e925a4a3d7 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -25,7 +25,6 @@ "node": ">=12.0" }, "dependencies": { - "@apollo/utils.keyvadapter": "^1.0.1", "@apollo/utils.keyvaluecache": "^1.0.1", "@apollo/utils.logger": "^1.0.0", "@apollo/utils.usagereporting": "^1.0.0", @@ -43,7 +42,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "keyv": "^4.3.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index bc7a0c9e025..1736f53a828 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -60,8 +60,7 @@ import { InternalPluginId, pluginIsInternal } from './internalPlugin'; import { newCachePolicy } from './cachePolicy'; import { GatewayIsTooOldError, SchemaManager } from './utils/schemaManager'; import * as uuid from 'uuid'; -import { KeyvAdapter } from '@apollo/utils.keyvadapter'; -import Keyv from 'keyv'; +import { UnboundedCache } from './utils/UnboundedCache'; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -711,7 +710,7 @@ export class ApolloServerBase< // random prefix each time we get a new schema. documentStore: this.config.documentStore === undefined - ? new KeyvAdapter(new Keyv()) + ? new UnboundedCache() : this.config.documentStore === null ? null : new PrefixingKeyValueCache( diff --git a/packages/apollo-server-core/src/__tests__/UnboundedCache.test.ts b/packages/apollo-server-core/src/__tests__/UnboundedCache.test.ts new file mode 100644 index 00000000000..cee09643d50 --- /dev/null +++ b/packages/apollo-server-core/src/__tests__/UnboundedCache.test.ts @@ -0,0 +1,35 @@ +import { UnboundedCache } from '../utils/UnboundedCache'; + +describe('UnboundedCache', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('basic get, set, delete', async () => { + const cache = new UnboundedCache(); + + await cache.set('key', 'value'); + expect(await cache.get('key')).toBe('value'); + + await cache.delete('key'); + expect(await cache.get('key')).toBeUndefined(); + }); + + it('get with ttl', async () => { + const cache = new UnboundedCache(); + + // 1s, or 1000ms + await cache.set('key', 'value', { ttl: 1 }); + + // check that it's there at 999ms + jest.advanceTimersByTime(999); + expect(await cache.get('key')).toBe('value'); + + // expire + jest.advanceTimersByTime(1); + expect(await cache.get('key')).toBeUndefined(); + }); +}); diff --git a/packages/apollo-server-core/src/__tests__/documentStore.test.ts b/packages/apollo-server-core/src/__tests__/documentStore.test.ts index 4f3f76cdc53..4570f1c89a9 100644 --- a/packages/apollo-server-core/src/__tests__/documentStore.test.ts +++ b/packages/apollo-server-core/src/__tests__/documentStore.test.ts @@ -2,8 +2,8 @@ import gql from 'graphql-tag'; import type { DocumentNode } from 'graphql'; import { ApolloServerBase } from '../ApolloServer'; -import { KeyvAdapter } from '@apollo/utils.keyvadapter'; import assert from 'assert'; +import { UnboundedCache } from '../utils/UnboundedCache'; const typeDefs = gql` type Query { @@ -54,7 +54,7 @@ describe('ApolloServerBase documentStore', () => { const options = await server.graphQLServerOptions(); const embeddedStore = options.documentStore; assert(embeddedStore); - expect(embeddedStore).toBeInstanceOf(KeyvAdapter); + expect(embeddedStore).toBeInstanceOf(UnboundedCache); await server.executeOperation(operations.simple.op); diff --git a/packages/apollo-server-core/src/utils/UnboundedCache.ts b/packages/apollo-server-core/src/utils/UnboundedCache.ts new file mode 100644 index 00000000000..5da7eadd2f4 --- /dev/null +++ b/packages/apollo-server-core/src/utils/UnboundedCache.ts @@ -0,0 +1,35 @@ +import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; + +export class UnboundedCache implements KeyValueCache { + constructor( + private cache: Map< + string, + { value: T; deadline: number | null } + > = new Map(), + ) {} + + async get(key: string) { + const entry = this.cache.get(key); + if (!entry) return undefined; + if (entry.deadline && entry.deadline <= Date.now()) { + await this.delete(key); + return undefined; + } + return entry.value; + } + + async set( + key: string, + value: T, + { ttl }: { ttl: number | null } = { ttl: null }, + ) { + this.cache.set(key, { + value, + deadline: ttl ? Date.now() + ttl * 1000 : null, + }); + } + + async delete(key: string) { + this.cache.delete(key); + } +}