Skip to content

Commit

Permalink
make serverless work
Browse files Browse the repository at this point in the history
  • Loading branch information
glasser committed Mar 16, 2021
1 parent b1c1e64 commit 5e54115
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 63 deletions.
13 changes: 2 additions & 11 deletions 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,
Expand All @@ -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
Expand All @@ -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) {
Expand Down
13 changes: 2 additions & 11 deletions 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,
Expand All @@ -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
Expand All @@ -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<string, any>;

if (cors) {
Expand Down
101 changes: 75 additions & 26 deletions packages/apollo-server-core/src/ApolloServer.ts
Expand Up @@ -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
Expand Down Expand Up @@ -447,22 +449,32 @@ 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
// call the protected `ensureStarting` when you first interact with them,
// 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<void> {
return await this._start();
}
Expand Down Expand Up @@ -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' ||
Expand All @@ -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));
}
}

Expand All @@ -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),
);
Expand Down Expand Up @@ -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<void> {
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(
[
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-core/src/index.ts
Expand Up @@ -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';

Expand Down
16 changes: 2 additions & 14 deletions packages/apollo-server-lambda/src/ApolloServer.ts
Expand Up @@ -8,10 +8,10 @@ import {
formatApolloErrors,
processFileUploads,
FileUploadOptions,
ApolloServerBase,
GraphQLOptions,
runHttpQuery,
HttpQueryError,
ApolloServerServerlessFrameworkBase,
} from 'apollo-server-core';
import {
renderPlaygroundPage,
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down

0 comments on commit 5e54115

Please sign in to comment.