Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hot schema reload with Apollo server v2 #1275

Closed
aliok opened this issue Jun 29, 2018 · 48 comments
Closed

Hot schema reload with Apollo server v2 #1275

aliok opened this issue Jun 29, 2018 · 48 comments
Labels
📝 documentation Focuses on changes to the documentation (docs)

Comments

@aliok
Copy link

aliok commented Jun 29, 2018

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.

@ghost ghost added the 📝 documentation Focuses on changes to the documentation (docs) label Jun 29, 2018
@evans
Copy link
Contributor

evans commented Jul 2, 2018

@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.

@aliok
Copy link
Author

aliok commented Jul 3, 2018

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.

@evans
Copy link
Contributor

evans commented Jul 11, 2018

@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
  })
}

@aliok
Copy link
Author

aliok commented Jul 13, 2018

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.

@mfellner
Copy link

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.

@aliok
Copy link
Author

aliok commented Jul 16, 2018

@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.

@iandvt
Copy link

iandvt commented Aug 25, 2018

@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.

@etamity
Copy link

etamity commented Dec 7, 2018

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.

@smolinari
Copy link

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

@chillenious
Copy link

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.

@inakianduaga
Copy link

inakianduaga commented Feb 8, 2019

@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
Copy link

@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.

@JacksonKearl
Copy link

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!

New request pipeline automation moved this from To do to Done Jul 2, 2019
@sarink
Copy link

sarink commented Jul 14, 2019

@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({ ... });
  }
})

@JacksonKearl
Copy link

JacksonKearl commented Jul 14, 2019

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.

@abernix
Copy link
Member

abernix commented Jul 17, 2019

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

@TdyP
Copy link

TdyP commented Jul 19, 2019

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 !

@JacksonKearl
Copy link

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))

@TdyP
Copy link

TdyP commented Jul 22, 2019

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

@spyshower
Copy link

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

@TdyP
Copy link

TdyP commented Aug 11, 2019

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

@ldiego08
Copy link

@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. :)

@TdyP
Copy link

TdyP commented Aug 30, 2019

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;

@emanuelschmitt
Copy link

@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 })

@zhuge-ei
Copy link

@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 :) )

@codeandgraphics
Copy link

codeandgraphics commented Sep 17, 2019

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

@kkotwal94
Copy link

kkotwal94 commented Sep 30, 2019

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 :)

@11111000000
Copy link

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

@loicmarie
Copy link

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.

@chillenious
Copy link

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

jmelis added a commit to jmelis/qontract-server that referenced this issue Apr 14, 2020
After upgrading the apollo-server-express version, any queries that
include interfaces will fail after reloading the server.

In order to reproduce we can just make a query like this one:

```
{
  permissions_v1 (path:"<path>") {
    service
    ... on PermissionGithubOrgTeam_v1 {
      org
    }
  }
}
```

Before reloading the object will contain `org`, but once we reload the
`org` parameter will be missing from the response.

This is because with the new apollo-server-express version besides
resetting the schema on a hot reload, we also need to make sure the
`schemaDerivedData` is also generated.

The solution to this issue was found here:
apollographql/apollo-server#1275 (comment)

Note the `@ts-ignore` line, which is needed because we are accessing a
private method `generateSchemaDerivedData`. Unless we have this line
typescript will raise an error when compiling.

This PR also adds a test that will catch this if there is a regression.
jmelis added a commit to app-sre/qontract-server that referenced this issue Apr 14, 2020
* add test to catch broken reload

* fix broken graphql after /reload

After upgrading the apollo-server-express version, any queries that
include interfaces will fail after reloading the server.

In order to reproduce we can just make a query like this one:

```
{
  permissions_v1 (path:"<path>") {
    service
    ... on PermissionGithubOrgTeam_v1 {
      org
    }
  }
}
```

Before reloading the object will contain `org`, but once we reload the
`org` parameter will be missing from the response.

This is because with the new apollo-server-express version besides
resetting the schema on a hot reload, we also need to make sure the
`schemaDerivedData` is also generated.

The solution to this issue was found here:
apollographql/apollo-server#1275 (comment)

Note the `@ts-ignore` line, which is needed because we are accessing a
private method `generateSchemaDerivedData`. Unless we have this line
typescript will raise an error when compiling.

This PR also adds a test that will catch this if there is a regression.
@mac2000
Copy link
Contributor

mac2000 commented Apr 30, 2020

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

@zhuge-ei
Copy link

@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))

@bigman73
Copy link

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)

@bigman73
Copy link

bigman73 commented Apr 29, 2021

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/apollo-server@2.9.1..apollo-server@2.9.2

Any ideas?

@mhassan1
Copy link

It looks like apollo-server-core moved schemaDerivedData into a new state object in #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 8b21f83 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 #1275 (comment), make sure your lock file is resolving to apollo-server-core@2.22.0 or higher, and then modify the implementation, as follows:

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

@bigman73
Copy link

bigman73 commented Apr 29, 2021

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 #1275 (comment) suggested, probably due to:

// This field is deprecated; users who are interested in learning
// their server's schema should instead make a plugin with serverWillStart,
// or register onSchemaChange on their gateway. It is only ever
// set for non-gateway servers.
this.schema = this.state.schemaDerivedData.schema;

@paradox37
Copy link

Why is this closed? I could easily reload schema with v1. But not with v2, which is ridiculous. Is there any better way then this suggested hack? Also, is this possible in v3 server? I could not find anything.

@cuzzlor
Copy link

cuzzlor commented Aug 12, 2021

Seems to work with v3 too, with a slightly different structure - thanks to the debugger and to all for the prior work!

image

@glasser
Copy link
Member

glasser commented Aug 17, 2021

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

As various comments above show, you can implement the same interface used by Apollo Gateway (including onSchemaChange, etc) without actually using @apollo/gateway yourself.

@bigman73
Copy link

Seems to work with v3 too, with a slightly different structure - thanks to the debugger and to all for the prior work!

image

Thanks, confirmed to work in v3!

For those who want to quickly paste the code:

  const schemaDerivedData = await apolloServer.generateSchemaDerivedData(newSchema)

  // Set new schema derived data
  set(apolloServer, 'schema', newSchema)
  set(apolloServer, 'state.schemaManager.schemaDerivedData', schemaDerivedData)

@bigman73
Copy link

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

As various comments above show, you can implement the same interface used by Apollo Gateway (including onSchemaChange, etc) without actually using @apollo/gateway yourself.

All the replies here that work above use a hack to set private fields in Apollo Server.
Perhaps you can expose an official API that us called swapSchema() that implements what the hack does.

@glasser
Copy link
Member

glasser commented Sep 22, 2021

There is an official API; it is the gateway/onSchemaChange API as shown in several comments above.

@cramatt
Copy link

cramatt commented Feb 24, 2022

@glasser you note that

There is an official API; it is the gateway/onSchemaChange API as shown in several comments above.

But my understanding is that gateway does not yet support subscriptions? If this is no longer true, I stand corrected! If this is still true, then it leaves users wanting subscriptions AND to update schema without an official API, and forces them to use the workaround of setting the private state variable.

@glasser
Copy link
Member

glasser commented Feb 25, 2022

Nothing in Apollo Server currently supports subscriptions; we do have documentation about how to run a single web server that happens to contain an Apollo Server and a subscription server that use the same schema. (We still hope to fix this eventually.)

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
📝 documentation Focuses on changes to the documentation (docs)
Projects
No open projects
Development

No branches or pull requests