From 5e54115eccff7f54e99f977cd4fa155c782463f6 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 16 Mar 2021 22:44:53 +0000 Subject: [PATCH] make serverless work --- .../src/ApolloServer.ts | 13 +-- .../src/ApolloServer.ts | 13 +-- .../apollo-server-core/src/ApolloServer.ts | 101 +++++++++++++----- packages/apollo-server-core/src/index.ts | 2 +- .../apollo-server-lambda/src/ApolloServer.ts | 16 +-- 5 files changed, 82 insertions(+), 63 deletions(-) diff --git a/packages/apollo-server-azure-functions/src/ApolloServer.ts b/packages/apollo-server-azure-functions/src/ApolloServer.ts index a9f7c1a11f3..6baa9774d81 100644 --- a/packages/apollo-server-azure-functions/src/ApolloServer.ts +++ b/packages/apollo-server-azure-functions/src/ApolloServer.ts @@ -1,6 +1,6 @@ import { Context, HttpRequest } from '@azure/functions'; import { HttpResponse } from 'azure-functions-ts-essentials'; -import { ApolloServerBase } from 'apollo-server-core'; +import { ApolloServerServerlessFrameworkBase } from 'apollo-server-core'; import { GraphQLOptions } from 'apollo-server-core'; import { renderPlaygroundPage, @@ -20,11 +20,7 @@ export interface CreateHandlerOptions { }; } -export class ApolloServer extends ApolloServerBase { - protected serverlessFramework(): boolean { - return true; - } - +export class ApolloServer extends ApolloServerServerlessFrameworkBase { // This translates the arguments from the middleware into graphQL options It // provides typings for the integration specific behavior, ideally this would // be propagated with a generic to the super class @@ -36,11 +32,6 @@ export class ApolloServer extends ApolloServerBase { } public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { - // In case the user didn't bother to call and await the `start` method, we - // kick it off in the background (with any errors getting logged - // and also rethrown from graphQLServerOptions during later requests). - this.ensureStarting(); - const corsHeaders: HttpResponse['headers'] = {}; if (cors) { diff --git a/packages/apollo-server-cloud-functions/src/ApolloServer.ts b/packages/apollo-server-cloud-functions/src/ApolloServer.ts index 4ae494e8a78..714bc33c130 100644 --- a/packages/apollo-server-cloud-functions/src/ApolloServer.ts +++ b/packages/apollo-server-cloud-functions/src/ApolloServer.ts @@ -1,4 +1,4 @@ -import { ApolloServerBase, GraphQLOptions } from 'apollo-server-core'; +import { ApolloServerServerlessFrameworkBase, GraphQLOptions } from 'apollo-server-core'; import { renderPlaygroundPage, RenderPageOptions as PlaygroundRenderPageOptions, @@ -18,11 +18,7 @@ export interface CreateHandlerOptions { }; } -export class ApolloServer extends ApolloServerBase { - protected serverlessFramework(): boolean { - return true; - } - +export class ApolloServer extends ApolloServerServerlessFrameworkBase { // This translates the arguments from the middleware into graphQL options It // provides typings for the integration specific behavior, ideally this would // be propagated with a generic to the super class @@ -34,11 +30,6 @@ export class ApolloServer extends ApolloServerBase { } public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { - // In case the user didn't bother to call and await the `start` method, we - // kick it off in the background (with any errors getting logged - // and also rethrown from graphQLServerOptions during later requests). - this.ensureStarting(); - const corsHeaders = {} as Record; if (cors) { diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index a672f787764..2219928fb68 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -396,10 +396,12 @@ export class ApolloServerBase { } if (gateway) { - // ApolloServer has been initialized but we have not yet tried to - // load the schema from the gateway. That will wait until the user - // calls `server.start()`, or until `ensureStarting` or `ensureStarted` - // are called. + // ApolloServer has been initialized but we have not yet tried to load the + // schema from the gateway. That will wait until the user calls + // `server.start()`, or until `ensureStarting` or `ensureStarted` are + // called. (In the case of an ApolloServerServerlessFrameworkBase + // subclass, `ensureStarting` is automatically called at the end of the + // constructor.) this.state = { phase: 'initialized with gateway', gateway }; // The main thing that the Gateway does is replace execution with @@ -447,7 +449,8 @@ export class ApolloServerBase { // to await a call to `start` immediately after creating your `ApolloServer`, // before attaching it to your web framework and starting to accept requests. // `start` should only be called once; if it throws and you'd like to retry, - // just create another `ApolloServer`. + // just create another `ApolloServer`. (Note that this paragraph does not + // apply to "serverless framework" integrations like Lambda.) // // For backwards compatibility with the pre-2.22 API, you are not required to // call start() yourself (this may change in AS3). Most integration packages @@ -455,14 +458,23 @@ export class ApolloServerBase { // which kicks off a "background" call to `start` if you haven't called it // yourself. Then `graphQLServerOptions` (which is called before processing) // each incoming GraphQL request) calls `ensureStarted` which waits for - // `start` to successfully complete (possibly by calling it itself), and throws - // a redacted error if `start` was not successful. If `start` is invoked - // implicitly by either of these mechanisms, any error that it throws will - // be logged when they occur and then again on every subsequent + // `start` to successfully complete (possibly by calling it itself), and + // throws a redacted error if `start` was not successful. If `start` is + // invoked implicitly by either of these mechanisms, any error that it throws + // will be logged when they occur and then again on every subsequent // `graphQLServerOptions` call (ie, every GraphQL request). Note that start - // failures are not recoverable without creating a new ApolloServer. You - // are highly encouraged to make these backwards-compatibility paths into - // no-ops by awaiting a call to `start` yourself. + // failures are not recoverable without creating a new ApolloServer. You are + // highly encouraged to make these backwards-compatibility paths into no-ops + // by awaiting a call to `start` yourself. + // + // Serverless integrations like Lambda (which extend + // `ApolloServerServerlessFrameworkBase`) do not support calling `start()`, + // because their lifecycle doesn't allow you to wait before assigning a + // handler or allowing the handler to be called. So they call `ensureStarting` + // at the end of the constructor, and don't really differentiate between + // startup failures and request failures. This is hopefully appropriate for a + // "serverless" framework. As above, startup failures result in returning a + // redacted error to the end user and logging the more detailed error. public async start(): Promise { return await this._start(); } @@ -578,15 +590,18 @@ export class ApolloServerBase { } } - // Part of the backwards-compatibility behavior described above `start` - // to make ApolloServer work if you don't explicitly call `start`. This is - // called by some of the integration frameworks when you interact with them - // (eg by calling applyMiddleware). - // It calls `start` for you if it hasn't been called yet, but doesn't wait - // for `start` to finish. The goal is that if you don't call `start` yourself - // the server should still do the rest of startup vaguely near when your server - // starts, not just when the first GraphQL request comes in. Without this call, - // startup wouldn't occur until `graphQLServerOptions` invokes `ensureStarted`. + // Part of the backwards-compatibility behavior described above `start` to + // make ApolloServer work if you don't explicitly call `start`. This is called + // by some of the integration frameworks when you interact with them (eg by + // calling applyMiddleware). It is also called from the end of the constructor + // for serverless framework integrations. + // + // It calls `start` for you if it hasn't been called yet, but doesn't wait for + // `start` to finish. The goal is that if you don't call `start` yourself the + // server should still do the rest of startup vaguely near when your server + // starts, not just when the first GraphQL request comes in. Without this + // call, startup wouldn't occur until `graphQLServerOptions` invokes + // `ensureStarted`. protected ensureStarting() { if ( this.state.phase === 'initialized with gateway' || @@ -598,7 +613,7 @@ export class ApolloServerBase { // Any thrown error will get logged, and also will cause // every call to graphQLServerOptions (ie, every GraphQL operation) // to log it again and prevent the operation from running. - this.start().catch((e) => this.logStartupError(e)); + this._start().catch((e) => this.logStartupError(e)); } } @@ -607,13 +622,18 @@ export class ApolloServerBase { // `graphQLServerOptions` had to initiate the startup process; if you call // `start` yourself (or you're using `apollo-server` whose `listen()` does // it for you) then you can handle the error however you'd like rather than - // this log occurring. + // this log occurring. (We don't suggest the use of `start()` for serverless + // frameworks because they don't support it.) private logStartupError(err: Error) { - this.logger.error( - 'Apollo Server was started implicitly and an error occurred during startup. ' + + const prelude = this.serverlessFramework() + ? 'An error occurred during Apollo Server startup.' + : 'Apollo Server was started implicitly and an error occurred during startup. ' + '(Consider calling `await server.start()` immediately after ' + '`server = new ApolloServer()` so you can handle these errors directly before ' + - 'starting your web server.) All GraphQL requests will now fail. The startup error ' + + 'starting your web server.)'; + this.logger.error( + prelude + + ' All GraphQL requests will now fail. The startup error ' + 'was: ' + ((err && err.message) || err), ); @@ -1215,6 +1235,35 @@ export class ApolloServerBase { } } +// There are two special things about Apollo Server subclasses built for +// serverless frameworks: +// - They use the serverlessFramework() method to inform plugins (in +// serverWillStart) that they are serverless, which the plugin can use to +// decide things like "by default, send usage reports after every request +// instead of on a timer" +// - Their main entry point (createHandler) generally needs to be called +// synchronously from the top level of your entry point, unlike (eg) +// applyMiddleware, so we can't expect you to `await server.start()` before +// calling it. So we kick off the start asynchronously from the constructor, +// and failures are logged and cause later requests to fail (in +// graphQLServerOptions). There's no way to make "the whole server fail" +// separately from making individual requests fail, but that's not +// entirely unreasonable for a "serverless" model. +export class ApolloServerServerlessFrameworkBase extends ApolloServerBase { + constructor(config: Config) { + super(config); + this.ensureStarting(); + } + protected serverlessFramework(): boolean { + return true; + } + public async start(): Promise { + throw new Error( + "When using an ApolloServer subclass from a serverless framework package, you don't need to call start(); just call createHandler().", + ); + } +} + function printNodeFileUploadsMessage(logger: Logger) { logger.error( [ diff --git a/packages/apollo-server-core/src/index.ts b/packages/apollo-server-core/src/index.ts index 111c40c608c..ae756071f9b 100644 --- a/packages/apollo-server-core/src/index.ts +++ b/packages/apollo-server-core/src/index.ts @@ -29,7 +29,7 @@ export { } from './playground'; // ApolloServer Base class -export { ApolloServerBase } from './ApolloServer'; +export { ApolloServerBase, ApolloServerServerlessFrameworkBase } from './ApolloServer'; export * from './types'; export * from './requestPipelineAPI'; diff --git a/packages/apollo-server-lambda/src/ApolloServer.ts b/packages/apollo-server-lambda/src/ApolloServer.ts index e49fe992357..dde6d5b37b6 100644 --- a/packages/apollo-server-lambda/src/ApolloServer.ts +++ b/packages/apollo-server-lambda/src/ApolloServer.ts @@ -8,10 +8,10 @@ import { formatApolloErrors, processFileUploads, FileUploadOptions, - ApolloServerBase, GraphQLOptions, runHttpQuery, HttpQueryError, + ApolloServerServerlessFrameworkBase, } from 'apollo-server-core'; import { renderPlaygroundPage, @@ -82,11 +82,7 @@ function maybeCallbackify( }; } -export class ApolloServer extends ApolloServerBase { - protected serverlessFramework(): boolean { - return true; - } - +export class ApolloServer extends ApolloServerServerlessFrameworkBase { // Uploads are supported in this integration protected supportsUploads(): boolean { return true; @@ -108,14 +104,6 @@ export class ApolloServer extends ApolloServerBase { onHealthCheck: undefined, }, ) { - // With serverless integrations we can't ask users to `await server.start()` - // immediately after the constructor, because we have to register the - // handler with the environment synchronously. So we just call `ensureStarting` - // here, which will initiate the startup process as soon as possible. - // Errors won't be caught until graphQLServerOptions is called, but that - // should be pretty soon anyway since this is serverless. - this.ensureStarting(); - const corsHeaders = new Headers(); if (cors) {