-
Notifications
You must be signed in to change notification settings - Fork 2k
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
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.
+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:
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 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.
|
hi @evans 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 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 |
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. |
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 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 One caveat is that Hope this helps! |
@JacksonKearl can you show how this would work with an actual gateway that connects to multiple services? via
|
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 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
This works pretty fine, my queries are executed properly, etc. 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. |
@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; |
@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 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:
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 |
It's what I'm using. Not great, but haven't found anything better. |
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.
* 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.
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 |
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
|
I had the hack proposed by @codeandgraphics working, but it is not working any more. Any ideas? |
It looks like To continue with the hot reloading implementation suggested in #1275 (comment), make sure your lock file is resolving to // Set new schema derived data
- set(apolloServer, 'schemaDerivedData', schemaDerivedData)
+ set(apolloServer, 'state.schemaDerivedData', schemaDerivedData) |
Thanks @mhassan1 It also seems to work fine without setting the 'schema' property, as the code in #1275 (comment) suggested, probably due to: apollo-server/packages/apollo-server-core/src/ApolloServer.ts Lines 425 to 429 in 5489bba
|
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. |
As various comments above show, you can implement the same interface used by Apollo Gateway (including onSchemaChange, etc) without actually using |
Thanks, confirmed to work in v3! For those who want to quickly paste the code:
|
All the replies here that work above use a hack to set private fields in Apollo Server. |
There is an official API; it is the gateway/onSchemaChange API as shown in several comments above. |
@glasser you note that
But my understanding is that |
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.) |
I am investigating how to do hot schema reload on Apollo server v2
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.
The text was updated successfully, but these errors were encountered: