From f53371e17b96b387c06ab1d3756dce60de46965e Mon Sep 17 00:00:00 2001 From: prathamesh0 <42446521+prathamesh0@users.noreply.github.com> Date: Thu, 17 Nov 2022 00:32:08 -0600 Subject: [PATCH] Accomodate GQL requests caching in code generator (#237) * Accomodate GQL requests caching in code generator * Add GQL API request queuing --- packages/address-watcher/src/server.ts | 4 +- packages/codegen/src/schema.ts | 25 ++++++++- .../src/templates/config-template.handlebars | 13 +++++ .../src/templates/readme-template.handlebars | 12 ++++ .../templates/resolvers-template.handlebars | 23 ++++++-- .../src/templates/server-template.handlebars | 4 +- packages/eden-watcher/src/server.ts | 4 +- packages/erc20-watcher/src/server.ts | 4 +- packages/erc721-watcher/src/server.ts | 4 +- packages/graph-test-watcher/src/server.ts | 4 +- packages/mobymask-watcher/src/server.ts | 4 +- packages/util/package.json | 1 + packages/util/src/misc.ts | 11 +++- packages/util/src/server.ts | 56 ++----------------- packages/util/src/types/common/main.d.ts | 6 ++ packages/util/src/types/common/package.json | 6 ++ yarn.lock | 25 ++++++++- 17 files changed, 130 insertions(+), 76 deletions(-) create mode 100644 packages/util/src/types/common/main.d.ts create mode 100644 packages/util/src/types/common/package.json diff --git a/packages/address-watcher/src/server.ts b/packages/address-watcher/src/server.ts index a18a4bb5..a7e8b291 100644 --- a/packages/address-watcher/src/server.ts +++ b/packages/address-watcher/src/server.ts @@ -39,8 +39,6 @@ export const main = async (): Promise => { assert(config.server, 'Missing server config'); - const { host, port } = config.server; - const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config; assert(dbConfig, 'Missing database config'); @@ -82,7 +80,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); return { app, server }; }; diff --git a/packages/codegen/src/schema.ts b/packages/codegen/src/schema.ts index 103477a0..004baea5 100644 --- a/packages/codegen/src/schema.ts +++ b/packages/codegen/src/schema.ts @@ -3,7 +3,7 @@ // import assert from 'assert'; -import { GraphQLSchema, parse, printSchema, print } from 'graphql'; +import { GraphQLSchema, parse, printSchema, print, GraphQLDirective, GraphQLInt, GraphQLBoolean } from 'graphql'; import { ObjectTypeComposer, ObjectTypeComposerDefinition, ObjectTypeComposerFieldConfigMapDefinition, SchemaComposer } from 'graphql-compose'; import { Writable } from 'stream'; import { utils } from 'ethers'; @@ -19,6 +19,7 @@ export class Schema { this._composer = new SchemaComposer(); this._events = []; + this._addGQLCacheTypes(); this._addBasicTypes(); } @@ -271,6 +272,28 @@ export class Schema { this._composer.addSchemaMustHaveType(typeComposer); } + _addGQLCacheTypes (): void { + // Create a enum type composer to add enum CacheControlScope in the schema composer. + const enumTypeComposer = this._composer.createEnumTC(` + enum CacheControlScope { + PUBLIC + PRIVATE + } + `); + this._composer.addSchemaMustHaveType(enumTypeComposer); + + // Add the directive cacheControl in the schema composer. + this._composer.addDirective(new GraphQLDirective({ + name: 'cacheControl', + locations: ['FIELD_DEFINITION', 'OBJECT', 'INTERFACE', 'UNION'], + args: { + maxAge: { type: GraphQLInt }, + inheritMaxAge: { type: GraphQLBoolean }, + scope: { type: enumTypeComposer.getType() } + } + })); + } + /** * Adds types 'ResultEvent' and 'WatchedEvent' to the schema. */ diff --git a/packages/codegen/src/templates/config-template.handlebars b/packages/codegen/src/templates/config-template.handlebars index 8c7f2b4d..63ea8ce3 100644 --- a/packages/codegen/src/templates/config-template.handlebars +++ b/packages/codegen/src/templates/config-template.handlebars @@ -30,6 +30,19 @@ # Use -1 for skipping check on block range. maxEventsBlockRange = 1000 + # GQL cache settings + [server.gqlCache] + enabled = true + + # Max in-memory cache size (in bytes) (default 8 MB) + # maxCacheSize + + # GQL cache-control max-age settings (in seconds) + maxAge = 15 + {{#if (subgraphPath)}} + timeTravelMaxAge = 86400 # 1 day + {{/if}} + [metrics] host = "127.0.0.1" port = 9000 diff --git a/packages/codegen/src/templates/readme-template.handlebars b/packages/codegen/src/templates/readme-template.handlebars index 10228260..344c8d6e 100644 --- a/packages/codegen/src/templates/readme-template.handlebars +++ b/packages/codegen/src/templates/readme-template.handlebars @@ -59,6 +59,18 @@ * Edit the custom hook function `createStateCheckpoint` (triggered just before default and CLI checkpoint) in [hooks.ts](./src/hooks.ts) to save the state in a `checkpoint` `State` using the `Indexer` object. +### GQL Caching + +To enable GQL requests caching: + +* Update the `server.gqlCache` config with required settings. + +* In the GQL [schema file](./src/schema.gql), use the `cacheControl` directive to apply cache hints at schema level. + + * Eg. Set `inheritMaxAge` to true for non-scalar fields of a type. + +* In the GQL [resolvers file](./src/resolvers.ts), uncomment the `setGQLCacheHints()` calls in resolvers for required queries. + ## Run * Run the watcher: diff --git a/packages/codegen/src/templates/resolvers-template.handlebars b/packages/codegen/src/templates/resolvers-template.handlebars index 97d932b9..b9496fbd 100644 --- a/packages/codegen/src/templates/resolvers-template.handlebars +++ b/packages/codegen/src/templates/resolvers-template.handlebars @@ -8,7 +8,7 @@ import debug from 'debug'; import Decimal from 'decimal.js'; import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'; -import { ValueResult, BlockHeight, gqlTotalQueryCount, gqlQueryCount, jsonBigIntStringReplacer, getResultState } from '@cerc-io/util'; +import { ValueResult, BlockHeight, gqlTotalQueryCount, gqlQueryCount, jsonBigIntStringReplacer, getResultState, setGQLCacheHints } from '@cerc-io/util'; import { Indexer } from './indexer'; import { EventWatcher } from './events'; @@ -22,6 +22,8 @@ const log = debug('vulcanize:resolver'); export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatcher): Promise => { assert(indexer); + const gqlCacheConfig = indexer.serverConfig.gqlCache; + return { BigInt: new BigInt('bigInt'), @@ -63,14 +65,22 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch Query: { {{#each queries}} - {{this.name}}: (_: any, { blockHash, contractAddress - {{~#each this.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string - {{~#each this.params}}, {{this.name}}: {{this.type~}} {{/each}} }): Promise => { + {{this.name}}: ( + _: any, + { blockHash, contractAddress + {{~#each this.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string + {{~#each this.params}}, {{this.name}}: {{this.type~}} {{/each}} }, + __: any, + info: GraphQLResolveInfo + ): Promise => { log('{{this.name}}', blockHash, contractAddress {{~#each this.params}}, {{this.name~}} {{/each}}); gqlTotalQueryCount.inc(1); gqlQueryCount.labels('{{this.name}}').inc(1); + // Set cache-control hints + // setGQLCacheHints(info, {}, gqlCacheConfig); + return indexer.{{this.name}}(blockHash, contractAddress {{~#each this.params}}, {{this.name~}} {{/each}}); }, @@ -79,7 +89,7 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch {{~#each subgraphQueries}} {{this.queryName}}: async ( - _: any, + _: any, { id, block = {} }: { id: string, block: BlockHeight }, __: any, info: GraphQLResolveInfo @@ -89,6 +99,9 @@ export const createResolvers = async (indexer: Indexer, eventWatcher: EventWatch gqlQueryCount.labels('{{this.queryName}}').inc(1); assert(info.fieldNodes[0].selectionSet); + // Set cache-control hints + // setGQLCacheHints(info, block, gqlCacheConfig); + return indexer.getSubgraphEntity({{this.entityName}}, id, block, info.fieldNodes[0].selectionSet.selections); }, diff --git a/packages/codegen/src/templates/server-template.handlebars b/packages/codegen/src/templates/server-template.handlebars index a4f9cf8f..ef635466 100644 --- a/packages/codegen/src/templates/server-template.handlebars +++ b/packages/codegen/src/templates/server-template.handlebars @@ -39,7 +39,7 @@ export const main = async (): Promise => { const config: Config = await getConfig(argv.f); const { ethClient, ethProvider } = await initClients(config); - const { host, port, kind: watcherKind } = config.server; + const { kind: watcherKind } = config.server; const db = new Database(config.database); await db.init(); @@ -85,7 +85,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); startGQLMetricsServer(config); diff --git a/packages/eden-watcher/src/server.ts b/packages/eden-watcher/src/server.ts index d2f32dc1..0a2fa975 100644 --- a/packages/eden-watcher/src/server.ts +++ b/packages/eden-watcher/src/server.ts @@ -13,7 +13,7 @@ import { hideBin } from 'yargs/helpers'; import debug from 'debug'; import 'graphql-import-node'; -import { DEFAULT_CONFIG_PATH, getConfig, Config, JobQueue, KIND_ACTIVE, initClients, startGQLMetricsServer, createAndStartServerWithCache } from '@cerc-io/util'; +import { DEFAULT_CONFIG_PATH, getConfig, Config, JobQueue, KIND_ACTIVE, initClients, startGQLMetricsServer, createAndStartServer } from '@cerc-io/util'; import { GraphWatcher, Database as GraphDatabase } from '@cerc-io/graph-node'; import { createResolvers } from './resolvers'; @@ -79,7 +79,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServerWithCache(app, typeDefs, resolvers, config.server); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); startGQLMetricsServer(config); diff --git a/packages/erc20-watcher/src/server.ts b/packages/erc20-watcher/src/server.ts index 5c38c620..bb76b9fa 100644 --- a/packages/erc20-watcher/src/server.ts +++ b/packages/erc20-watcher/src/server.ts @@ -37,7 +37,7 @@ export const main = async (): Promise => { const config: Config = await getConfig(argv.f); const { ethClient, ethProvider } = await initClients(config); - const { host, port, kind: watcherKind } = config.server; + const { kind: watcherKind } = config.server; const db = new Database(config.database); await db.init(); @@ -70,7 +70,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); return { app, server }; }; diff --git a/packages/erc721-watcher/src/server.ts b/packages/erc721-watcher/src/server.ts index df9c84e6..bc73c1ec 100644 --- a/packages/erc721-watcher/src/server.ts +++ b/packages/erc721-watcher/src/server.ts @@ -36,7 +36,7 @@ export const main = async (): Promise => { const config: Config = await getConfig(argv.f); const { ethClient, ethProvider } = await initClients(config); - const { host, port, kind: watcherKind } = config.server; + const { kind: watcherKind } = config.server; const db = new Database(config.database); await db.init(); @@ -70,7 +70,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); return { app, server }; }; diff --git a/packages/graph-test-watcher/src/server.ts b/packages/graph-test-watcher/src/server.ts index b288a052..a2deaea0 100644 --- a/packages/graph-test-watcher/src/server.ts +++ b/packages/graph-test-watcher/src/server.ts @@ -37,7 +37,7 @@ export const main = async (): Promise => { const config: Config = await getConfig(argv.f); const { ethClient, ethProvider } = await initClients(config); - const { host, port, kind: watcherKind } = config.server; + const { kind: watcherKind } = config.server; const db = new Database(config.database); await db.init(); @@ -79,7 +79,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); return { app, server }; }; diff --git a/packages/mobymask-watcher/src/server.ts b/packages/mobymask-watcher/src/server.ts index 2a8c29cd..30235e4f 100644 --- a/packages/mobymask-watcher/src/server.ts +++ b/packages/mobymask-watcher/src/server.ts @@ -36,7 +36,7 @@ export const main = async (): Promise => { const config: Config = await getConfig(argv.f); const { ethClient, ethProvider } = await initClients(config); - const { host, port, kind: watcherKind } = config.server; + const { kind: watcherKind } = config.server; const db = new Database(config.database); await db.init(); @@ -70,7 +70,7 @@ export const main = async (): Promise => { // Create an Express app const app: Application = express(); - const server = createAndStartServer(app, typeDefs, resolvers, { host, port }); + const server = createAndStartServer(app, typeDefs, resolvers, config.server); startGQLMetricsServer(config); diff --git a/packages/util/package.json b/packages/util/package.json index ee01cd93..6e5fa427 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -15,6 +15,7 @@ "decimal.js": "^10.3.1", "ethers": "^5.4.4", "express": "^4.18.2", + "express-queue": "^0.0.13", "fs-extra": "^10.0.0", "graphql": "^15.5.0", "graphql-ws": "^5.11.2", diff --git a/packages/util/src/misc.ts b/packages/util/src/misc.ts index 1701e1ba..c7816a59 100644 --- a/packages/util/src/misc.ts +++ b/packages/util/src/misc.ts @@ -296,9 +296,14 @@ export const setGQLCacheHints = (info: GraphQLResolveInfo, block: BlockHeight, g return; } - assert(gqlCache.maxAge, 'Missing server gqlCache.maxAge'); - assert(gqlCache.timeTravelMaxAge, 'Missing server gqlCache.timeTravelMaxAge'); + let maxAge: number; + if (_.isEmpty(block)) { + assert(gqlCache.maxAge, 'Missing server gqlCache.maxAge'); + maxAge = gqlCache.maxAge; + } else { + assert(gqlCache.timeTravelMaxAge, 'Missing server gqlCache.timeTravelMaxAge'); + maxAge = gqlCache.timeTravelMaxAge; + } - const maxAge = _.isEmpty(block) ? gqlCache.maxAge : gqlCache.timeTravelMaxAge; info.cacheControl.setCacheHint({ maxAge }); }; diff --git a/packages/util/src/server.ts b/packages/util/src/server.ts index 1af8b9f0..efda091b 100644 --- a/packages/util/src/server.ts +++ b/packages/util/src/server.ts @@ -7,6 +7,7 @@ import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core'; import debug from 'debug'; import responseCachePlugin from 'apollo-server-plugin-response-cache'; import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache'; +import queue from 'express-queue'; import { TypeSource } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -16,15 +17,15 @@ import { ServerConfig } from './config'; const log = debug('vulcanize:server'); -export const createAndStartServerWithCache = async ( +export const createAndStartServer = async ( app: Application, typeDefs: TypeSource, resolvers: any, serverConfig: ServerConfig ): Promise => { - const host = serverConfig.host; - const port = serverConfig.port; - const gqlCacheConfig = serverConfig.gqlCache; + const { host, port, gqlCache: gqlCacheConfig, maxSimultaneousRequests, maxRequestQueueLimit } = serverConfig; + + app.use(queue({ activeLimit: maxSimultaneousRequests || 1, queuedLimit: maxRequestQueueLimit || -1 })); // Create HTTP server const httpServer = createServer(app); @@ -76,50 +77,3 @@ export const createAndStartServerWithCache = async ( return server; }; - -export const createAndStartServer = async ( - app: Application, - typeDefs: TypeSource, - resolvers: any, - endPoint: { host: string, port: number } -): Promise => { - // Create HTTP server - const httpServer = createServer(app); - - // Create the schema - const schema = makeExecutableSchema({ typeDefs, resolvers }); - - // Create our WebSocket server using the HTTP server we just set up. - const wsServer = new WebSocketServer({ - server: httpServer, - path: '/graphql' - }); - const serverCleanup = useServer({ schema }, wsServer); - - const server = new ApolloServer({ - schema, - csrfPrevention: true, - plugins: [ - // Proper shutdown for the HTTP server - ApolloServerPluginDrainHttpServer({ httpServer }), - // Proper shutdown for the WebSocket server - { - async serverWillStart () { - return { - async drainServer () { - await serverCleanup.dispose(); - } - }; - } - } - ] - }); - await server.start(); - server.applyMiddleware({ app }); - - httpServer.listen(endPoint.port, endPoint.host, () => { - log(`Server is listening on ${endPoint.host}:${endPoint.port}${server.graphqlPath}`); - }); - - return server; -}; diff --git a/packages/util/src/types/common/main.d.ts b/packages/util/src/types/common/main.d.ts new file mode 100644 index 00000000..950c45b5 --- /dev/null +++ b/packages/util/src/types/common/main.d.ts @@ -0,0 +1,6 @@ +// +// Copyright 2022 Vulcanize, Inc. +// + +// https://medium.com/@steveruiz/using-a-javascript-library-without-type-declarations-in-a-typescript-project-3643490015f3 +declare module 'express-queue'; diff --git a/packages/util/src/types/common/package.json b/packages/util/src/types/common/package.json new file mode 100644 index 00000000..5861d0f0 --- /dev/null +++ b/packages/util/src/types/common/package.json @@ -0,0 +1,6 @@ +{ + "name": "common", + "version": "0.1.0", + "license": "AGPL-3.0", + "typings": "main.d.ts" +} diff --git a/yarn.lock b/yarn.lock index 11c960b4..cca3f393 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5660,7 +5660,7 @@ debug@4, debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@4.3.4, debug@^4.3.3: +debug@4.3.4, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7073,6 +7073,22 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +express-end@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/express-end/-/express-end-0.0.8.tgz#0c8fd95428932158f2b4cf91f4045346bf2c5323" + integrity sha512-PPntzICAq006LBpXKBVJtmRUiCRqTMZ+OB8L2RFXgx+OmkMWU66IL4DTEPF/DOcxmsuC7Y0NdbT2R71lb+pBpg== + dependencies: + debug "^2.2.0" + +express-queue@^0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/express-queue/-/express-queue-0.0.13.tgz#e9800d67749d4dfab7c34223f00595af933ce5df" + integrity sha512-C4OEDasGDqpXLrZICSUxbY47p5c0bKqf/3/3hwauSCmI+jVVxKBWU2w39BuKLP6nF65z87uDFBbJMPAn2ZrG3g== + dependencies: + debug "^4.3.4" + express-end "0.0.8" + mini-queue "0.0.14" + express@^4.14.0: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -10371,6 +10387,13 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +mini-queue@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/mini-queue/-/mini-queue-0.0.14.tgz#83d2f3f908e3cac5390bd986d1e6fbbfa0d95b93" + integrity sha512-DNh9Wn8U1jrmn1yVfpviwClyER/Y4ltgGbG+LF/KIdKJ8BEo2Q9jDDPG7tEhz6F/DTZ/ohv5D7AAXFVSFyP05Q== + dependencies: + debug "^3.1.0" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"