Apollo-server: Hot schema reload with Apollo server v2

Created on 29 Jun 2018  ·  40Comments  ·  Source: apollographql/apollo-server

I am investigating how to do hot schema reload on Apollo server v2
```
const server = new ApolloServer({buildSchema()})
server.applyMiddleware({app})

setTimeout(function () {
server.schema = buildSchema()
}, 10000)
````
Imagine in that 10 seconds, the schema is changed (coming from network/db/whatever)
Approach above seems to work fine, but any better ideas?
What about open websocket connections from clients?

I am thinking of another way for this in which the Apollo server is stopped and recreated. However, not sure what would happen to open websocket connections from clients.

documentation

Most helpful comment

I don't understand why this is closed.

Not everyone is interested in using Apollo Gateway, but some still want to build a custom gateway using Apollo Server.

A simple public method named reloadSchema should change the schema, using the logic shown above, but without the hacks of calling a private method and setting private fields

const schema = await createSchema() // Custom schema logic, stitching, etc.

apolloServer.reloadSchema(schema)

All 40 comments

@aliok That will work, you're using an internal implementation detail, so I'd like to have a better solution that will hold up longer term. It sounds like using a new schema every 10 seconds is a hard requirement. What about it changes during those updates? Depending on how it changes, we might be able to make the updates in a different location.

Ideally the schema type definitions stay the same. If the backend endpoints are changing, then updating the data source/connector layer might be the best option.

Thanks for the reply @evans

At AeroGear community, we are building a data sync service for mobile apps, leveraging GraphQL and Apollo.
https://github.com/aerogear/aerogear-data-sync-server

That will work, you're using an internal implementation detail, so I'd like to have a better solution that will hold up longer term.

+1 on that!

In the end product, we will have an admin UI that will allow users to update config like schemas, datasources and resolvers. And whenever that happens, we need to use those new stuff without restarting the backend service (this is the GraphQL server using Apollo).

To classify, what I am trying to find out is basically is 2 things:

  1. How to hot reload those changes in Apollo server properly?
  2. What happens to existing websocket connections and requests that are currently being executed?

I am not expecting to find a definitive answer to question#2. More like thinking about what would happen.

@aliok for now there isn't a way to do schema hot reloading with the subscription server. You could create a new instance of ApolloServer with another middleware. Here's an example of how one person did hot reloading with an http listener.

The current solution for hot reloading a schema with websockets is definitely an open question. We're thinking about creating a more structured request pipeline that would enable this sort of hot swap out with streaming requests.

import http from 'http'
import app from './app'

const PORT = 4000
const server = http.createServer(app)

server.listen(PORT, () => {
  console.log(`GraphQL-server listening on port ${PORT}.`)
})

let currentApp = app
if (module.hot) {
  module.hot.accept(['./app', './schema'], () => {
    server.removeListener('request', currentApp)
    server.on('request', app)
    currentApp = app
  })
}

hi @evans
Thanks a lot for the response.

I was thinking about recreating the things at the middleware level, but recreating the app is a good idea!

I will post my findings.

I would also like to dynamically update the schema at runtime (stitching together multiple 3rd party backends) and I'm using apollo-server-micro. It should be possible to (re)load the schema on every request by directly using microApollo (similar to the old microGraphql v1):

import { graphqlMicro } from 'apollo-server-micro/dist/microApollo';

const handler = graphqlMicro(
  async (req?: IncomingMessage): Promise<GraphQLOptions> => {
    const schema = await getLatestSchema();
    return { schema };
  }
);

However with this approach all the new v2 functionality is lost...

A better approach might be to simply recreate and swap out the middleware instance every time the schema was really changed.

@evans implemented hot reload as your instructions here: https://github.com/aerogear/data-sync-server/blob/apollo-v2/server.js#L53

It works nice. As you wrote, what happens to open connections is still unknown.

I think you can close this issue. I can create a new one if I have any problems in the future.

@mfellner - Have you found a way to handle dynamically generated schemas at runtime? I have a working implementation in Apollo Server 1 but it seems like this type of functionality is impossible in the current Apollo Server 2 implementation.

Here is my solution, it works perfectly.

    app.use('/graphql', (req, res, next)=>{
        const buildSchema = require('./backend/graphql/schema');
        apolloServer.schema = buildSchema();
        next();
    })
    apolloServer.applyMiddleware({ app, path: '/graphql' });

basically every time hit the /graphql endpoint will rebuild schema, and re-assign it, the pass through it to apollo server.

That seems like a lot of unnecessary computation to me. Wouldn't it be better to find a way to flag when a rebuild should happen? Like, only when a schema change has actually been made?

Scott

Speaking for my own case, I actually need a schema that varies per client (more like a partial schema based on their auth profile). Aside from doing routing tricks, this is the only way I see to do this.

@chillenious

Speaking for my own case, I actually need a schema that varies per client (more like a partial schema based on their auth profile). Aside from doing routing tricks, this is the only way I see to do this.

I need the same. Does the solution above work w/ concurrency? Wouldn't mutating the apollo server affect about-to-be processed requests? (so if user A sends a request and user B sends another request very shortly after, user A is served the schema from user B?

@chillenious

Speaking for my own case, I actually need a schema that varies per client (more like a partial schema based on their auth profile). Aside from doing routing tricks, this is the only way I see to do this.

I need the same. Does the solution above work w/ concurrency? Wouldn't mutating the apollo server affect about-to-be processed requests? (so if user A sends a request and user B sends another request very shortly after, user A is served the schema from user B?

Not really an issue for me, as I'm using cloud functions (so I'm not reusing an instance). I can imagine this could be a problem elsewhere though.

With ApolloServer 2.7, you will be able to pass a GraphQLService to the gateway parameter of ApolloServer's config. It should look something like:

export type Unsubscriber = () => void;
export type SchemaChangeCallback = (schema: GraphQLSchema) => void;

export type GraphQLServiceConfig = {
  schema: GraphQLSchema;
  executor: GraphQLExecutor;
};

export interface GraphQLService {
  load(): Promise<GraphQLServiceConfig>;
  onSchemaChange(callback: SchemaChangeCallback): Unsubscriber;
}

You can check the apollo-gateway package for an example implementation of this service. As you aren't providing any new execution strategy, you can just use graphql.execute .

One caveat is that subscriptions are disabled when an ApolloServer is operating as a gateway.

Hope this helps!

@JacksonKearl can you show how this would work with an actual gateway that connects to multiple services? via

new ApolloGateway({
  serviceList: [ ... ],
  buildService({ name, url }) {
    return new RemoteGraphQLDataSource({ ... });
  }
})

Hey @sarink, we're actually right in the middle of writing docs for all this, but they should come out in the coming week!

In short: an ApolloGateway operating using a fixed serviceList config won't update itself to reflect downstream changes. This is done for a number of reasons, but the long and the short of it is that gateway reliability shouldn't depend on service reliability.

We'll be releasing more info how to set up an automatically updating gateway (what we're calling "Managed Federation") in a robust manner soon, but the general idea is to use a remote registry of services and schemas that will push out a schema change to the gateway only when all component service updates have validated against all other component services, such that the gateway, and therefore your clients, never see an invalid state. This registry and the associated remote rollout process will be released as a free component of Apollo Engine.

Hopefully the new managed federation documentation and the accompanying blog post help to better explain this!

Hi there
All of this looks very promising but I'm still struggling to hot reload my schema (as in the original question).
I've just one service, no federation here thus I'm using a GraphQLService in gateway as suggested by @JacksonKearl

const mySchema = graphqlApp.schema, // This is a getter who always returns my up-to-date schema
const apolloServ = new ApolloServer({
    gateway: {
        load: () => {
            return Promise.resolve({
                schema: mySchema
                executor: args => {
                    return execute({ // graphql.execute
                        ...args,
                        schema: mySchema,
                        contextValue: args.context
                    });
                }
            });
        },
        onSchemaChange: () => {
            // How is it called?
            return () => null;
        }
    },
    subscriptions: false
});

This works pretty fine, my queries are executed properly, etc.
When my schema changes, an introspection query give me the up-to-date schema, that's all fine. But an actual query calling a new field is failing with a Cannot query field... as it was validating against the old schema.
Is there a cache somewhere which is not refreshed? Am I using it the right way?
All of this is not very clear for me, for example how to get onSchemaChange called?

Tested on Apollo 2.7 with Hapi

Thanks !

You can check the ApolloServer constructor on versions past 2.7 for more details, but it's basically:

let triggerUpdate: () => void;
const reloader: GraphQLService {
  load: async () => ({schema: mySchema, executor: graphql.execute}),
  onSchemaChange(callback) { triggerUpdate = callback },
}

// Idk how you're triggering, but you can imagine something like:
process.on('updateSchema', schema => triggerUpdate(schema))

Ooh ok, got it! Thanks @JacksonKearl , I finally managed to get Apollo Server v2 fully working :)

@TdyP Could you post your final code for this? :)

Sorry I'm on vacation right now thus don't have access to my code for the next 2 weeks. But the principle is what @JacksonKearl posted above.
I'm emitting an event where I update my schema, then I bind the callback supplied by onSchemaChange to this event. This callback is what actually update the schema for Apollo

@TdyP it would be super helpful to have an example of how you got this working. I'm trudging through this exact same issue atm. :)

Sure, here is the most important part.

Server:

import {myEmitter} from 'mySchemaManager';

const apolloServ = new ApolloServer({
    gateway: {
        load: () => {
            return Promise.resolve({
                schema: schema,
                executor: args => {
                    return graphql.execute({
                        ...args,
                        schema: schema
                    });
                }
            });
        },
        /**
            * The callback received here is an Apollo internal function which actually update
            * the schema stored by Apollo Server. We init an event listener to execute this function
            * on schema update
            */
        onSchemaChange: callback => {
            myEmitter.on('schema_update', callback);

            return () => myEmitter.off('schema_update', callback);
        }
    }
});

Schema manager:

const myEmitter = new EventEmitter();
const generateSchema = () => {
    const schema = {.. } // ... Update your schema ...
    myEmitter.emit('schema_update', schema);
}
export myEmitter;

Thanks, @TdyP !!

Is anyone able to share a full working version of this? All the examples shared seem to illustrate a part of the picture, but it's not always clear which comments in the discussion are compatible with other comments, or what all the variables in an example are e.g. @TdyP's example uses a schema variable in the load() method, but I'm not sure where this comes from.

I think I've almost got it working, but now getting UnhandledPromiseRejectionWarning: Error: Expected { engine: undefined } to be a GraphQL schema, and not really sure what I need to change

@wheresrhys the schema variable contains the actual updated schema. In my case, I gather around different typeDefs and resolvers coming from different files and process it with makeExecutableSchema(). This is what works in my case, you problably retrieve it in a different way.

Thanks. Yep I figured it out in the end. Got stuck for ages setting context
as graphql.execute expects contextValue not context - took ages to spot
that in the docs

On Thu, 5 Sep 2019 at 3:12 pm, Teddy Paul notifications@github.com wrote:

@wheresrhys https://github.com/wheresrhys the schema variable contains
the actual updated schema. In my case, I gather around different typeDefs
and resolvers coming from different files and process it with
makeExecutableSchema(). This is what works in my case, you problably
retrieve it in a different way.


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/apollographql/apollo-server/issues/1275?email_source=notifications&email_token=AADNIRY3B5I3I36LB2WTBFTQIEATDA5CNFSM4FHUDLZ2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD57BI2I#issuecomment-528356457,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AADNIR2DRBQPC6WTBSMZ2XDQIEATDANCNFSM4FHUDLZQ
.

@TdyP Thanks for sharing your solution! The solution worked fine for Queries/Mutations where the variables where inlined, however for operations where the variables where external, I had to add them to the execute command via variableValues.

This is my solution:

const gateway: GraphQLService = {
    load: async () =>
      ({
        schema,
        executor: args =>
          execute({
            ...args,
            schema,
            contextValue: args.context,
            variableValues: args.request.variables, <---- Adding the variables from the request
          }),
      } as GraphQLServiceConfig),
    onSchemaChange: callback => {
      eventEmitter.on(SCHEMA_UPDATE_EVENT, callback);
      return () => eventEmitter.off(SCHEMA_UPDATE_EVENT, callback);
    },
  };

const apolloServer = new ApolloServer({ gateway })

@emanuelschmitt 😍 ❤️ although i reached it today by myself but seeing someone care enough to share his code here made my day(i came here to do this to :) )

If you can't use gateway/federation for some reasons (subscriptions, typegraphql, etc.) and you want to upgrade schema in runtime for Apollo Server, you can just do this:

      // import set from 'lodash/set'
      // import { mergeSchemas } from 'graphql-tools'
      // apolloServer = new ApolloServer({ schema, ... })
      // createSchema = mergeSchemas({ schemas: [...], ... })

      const schema = await createSchema()
      // Got schema derived data from private ApolloServer method
      // @ts-ignore
      const schemaDerivedData = await apolloServer.generateSchemaDerivedData(
        schema,
      )
      // Set new schema
      set(apolloServer, 'schema', schema)
      // Set new schema derived data
      set(apolloServer, 'schemaDerivedData', schemaDerivedData)

And all refresh stuff will work like a charm

Im able to do regular queries / mutations, however i keep getting schema is not configured for subscriptions. @codeandgraphics

EDIT: I had to do something like this:

export async function pollSchemas(apollo: any) {
  let stitched = false;
  do {
    try {
      console.log("Stitching schemas.....");
      const schema = await stitchSchema(schemaLinks());
      stitched = true;
      const schemaDerivedData = await apollo.generateSchemaDerivedData(schema);
      set(apollo, "schema", schema);
      set(apollo, "schemaDerivedData", schemaDerivedData);
      set(apollo, ["subscriptionServer", "schema"], schema);
      console.log(apollo.subscriptionServer);
      console.log("Schemas stitched!");
    } catch (e) {
      console.log(e);
      await new Promise(done => setTimeout(done, 2500));
    }
  } while (!stitched);
}

Setting the subscription server schema as well fixed it for me :)

why that closed? Can't find any docs about hot replacing resolvers, data-sources...

Can someone confirm that @codeandgraphics solution is the way to go if we cannot use gateway ?

Works like a charm, but seems à little hackish. Moreover generateSchemaDerivedData method is private in typings.

It's what I'm using. Not great, but haven't found anything better.

Another little bit hacky workaround is:

const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require("@apollo/gateway");

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'accounts', url: 'http://localhost:4001' },
    { name: 'articles', url: 'http://localhost:4002' }
  ]
});

const server = new ApolloServer({ 
  gateway,
  subscriptions: false,
  plugins: [
    {
      requestDidStart: (requestContext) => {
        if (requestContext.request.http.headers.get('X-Reload-Gateway') && requestContext.request.operationName === 'reload') {
          gateway.load()
        }
      }
    }
  ]
});

server.listen()

So now I can just send something like query reload { __type(name:"String") { name } } with X-Reload-Gateway header directly from playground and schema will be indeed updated

@TdyP Thanks for sharing your solution! The solution worked fine for Queries/Mutations where the variables where inlined, however for operations where the variables where external, I had to add them to the execute command via variableValues.

This is my solution:

const gateway: GraphQLService = {
    load: async () =>
      ({
        schema,
        executor: args =>
          execute({
            ...args,
            schema,
            contextValue: args.context,
            variableValues: args.request.variables, <---- Adding the variables from the request
          }),
      } as GraphQLServiceConfig),
    onSchemaChange: callback => {
      eventEmitter.on(SCHEMA_UPDATE_EVENT, callback);
      return () => eventEmitter.off(SCHEMA_UPDATE_EVENT, callback);
    },
  };

const apolloServer = new ApolloServer({ gateway })

this is not working anymore (upgraded from 2.9 to 2.16 and it's broken(the reason is that it says that gateway it self needs an eeutor))

I don't understand why this is closed.

Not everyone is interested in using Apollo Gateway, but some still want to build a custom gateway using Apollo Server.

A simple public method named reloadSchema should change the schema, using the logic shown above, but without the hacks of calling a private method and setting private fields

const schema = await createSchema() // Custom schema logic, stitching, etc.

apolloServer.reloadSchema(schema)

I had the hack proposed by @codeandgraphics working, but it is not working any more.
It's not doing anything, tried with apollo-server 2.10.0 .. 2.23.0
It DOES work with 2.9.1, and broken with 2.9.2
https://github.com/apollographql/apollo-server/compare/[email protected]@2.9.2

Any ideas?

It looks like apollo-server-core moved schemaDerivedData into a new state object in https://github.com/apollographql/apollo-server/pull/4981 as part of apollo-server-core release 2.22.0. The reason why upgrading from 2.9.1 to 2.9.2 gets this change is that https://github.com/apollographql/apollo-server/commit/8b21f83286c68fa9a8f63c781244ee511d9a52f5 changed the publishing behavior such that apollo-server started using a ^ in its dependency on apollo-server-core.

To continue with the hot reloading implementation suggested in https://github.com/apollographql/apollo-server/issues/1275#issuecomment-532183702, make sure your lock file is resolving to [email protected] or higher, and then modify the implementation, as follows:

// Set new schema derived data
- set(apolloServer, 'schemaDerivedData', schemaDerivedData)
+ set(apolloServer, 'state.schemaDerivedData', schemaDerivedData)

Thanks @mhassan1
Your suggestion works perfect
I tested with apollo-server 2.23.0

It also seems to work fine without setting the 'schema' property, as the code in https://github.com/apollographql/apollo-server/issues/1275#issuecomment-532183702 suggested, probably due to:
https://github.com/apollographql/apollo-server/blob/5489bba4c196b4621a37c7319653626512c49fe9/packages/apollo-server-core/src/ApolloServer.ts#L425-L429

Was this page helpful?
0 / 5 - 0 ratings