-
Notifications
You must be signed in to change notification settings - Fork 257
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
gateway-js: Add onSchemaLoadOrUpdate() method to register listeners for schema loads/updates and give them the core supergraph SDL #738
Conversation
Is there a high level design of "gateway schema reporting" available? |
@glasser I've made the PR to update the plugin at apollographql/apollo-server#5187 ; that PR's description goes more into detail about the approach used. |
@@ -40,7 +40,11 @@ import { getVariableValues } from 'graphql/execution/values'; | |||
import fetcher from 'make-fetch-happen'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what it's worth: I'm personally a big fan of prettier, but others who work more closely on this project are less so (or at least, about introducing prettier-only changes). Definitely don't squash this PR when you merge it. I think @trevor-scheer works on this file most closely (eg, maybe he has some outstanding changes this would clash with) — Trevor, do you think having a prettier commit in this PR is helpful or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either is ok with me. I'm personally a fan of it, but the state of prettier
in this repo (unenforced) puts me on the fence and I don't feel too strongly in any case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, I'm also a fan of prettier
. I'd be fine with keeping the prettier
commit in the PR, but unsquashed as @glasser suggested. (If we do have a large rewrite pending that clashes, let me know and I can drop the commit.)
gateway-js/src/index.ts
Outdated
// TODO(trevor): #580 redundant parse | ||
this.parsedSupergraphSdl = parse(supergraphSdl); | ||
this.queryPlanner = new QueryPlanner(schema); | ||
this.updateWithSchemaAndNotify(schema); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a good idea to make loadStatic and loadDynamic more consistent, but I think this change may be in the wrong direction. I think it would make more sense if updateSchema didn't call the listeners when invoked via loadDynamic (and only when invoked on polling). ie, the onSchemaChange callback should truly be about schema change, not also the initial load. (It might be reasonable to do it this way if load() didn't return a schema and onSchemaChange was the only way the schema was delivered, but otherwise I don't think we need a no-op onSchemaChange on every startup.)
I think that it's a no-op in the full system including apollo-server, because apollo-server's onSchemaChange callback (at least as of v2.22) does nothing if the server is not yet in the started state.
@trevor-scheer do you agree?
(Factoring out the listener invocation code seems reasonable either way. I believe we left a fair amount of copy-and-paste because we expected to be able to get rid of some of the code soon, but maybe that isn't the case any more.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think the bit about load
is a fair point but our current API encourages users to specifically not handle calling load
themselves and to let AS handle that. Still totally fine to do it the "old way", but at this point I think it's undocumented.
Is there a reason you think it's bad to call the update listeners on initial load? Noisy/not semantic maybe? I can appreciate that viewpoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, I agree that this API is for use by AS, and AS explicitly doesn't care about getting onSchemaChange callbacks called on the initial load (its callback does nothing if the server isn't started yet). So the fact that it's for use by AS suggests that if we want to resolve the inconsistency in the current behavior (which I think is a good idea) we should do it in the direction of fewer redundant calls rather than more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onSchemaChange
is not necessarily just for AS, that could reasonably be used in userland for observability (though I see now it's undocumented). We use it for promise driven tests, so we may need to update some if its behavior is changed.
I definitely agree we should make it consistent. I also agree less redundant calls is better, though I'm not convinced this is redundant (for the userland use case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trevor-scheer OK, I'm back from the long weekend, let's resolve this :) Sounds like we agree it should be consistent.
My instinct is that calling the onSchemaChange
before load
resolves with the original schema seems odd to me, and not a great fit for the "change" in its name. So I lean towards not calling it for the original load. But this is an area you are more deeply involved with, so how about you choose which way to make it consistent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @trevor-scheer here. While our own code only uses onSchemaChange()
for schema reporting, it's part of the public API (albeit undocumented) and load()
is expected to be called by Apollo Server, so I'd err on the side of exposing the initial schema load to onSchemaChange
callbacks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See somewhat rambly apollographql/apollo-server#5187 (comment) for an idea of how to resolve this: basically, I think y'all are right that calling it always is reasonable, but let's make a more clear name for it. Coming up with a new name also gives us the opportunity to make this callback take an object rather than positional arguments, and to more easily detect whether the new version of the gateway is running.
(If you go through with that plan, then I think it's fine to consistently call callbacks registered either via the new name or the old name with the first schema.)
gateway-js/src/index.ts
Outdated
@@ -703,7 +702,7 @@ export class ApolloGateway implements GraphQLService { | |||
}; | |||
} | |||
|
|||
public onSchemaChange(callback: SchemaChangeCallback): Unsubscriber { | |||
public onSchemaChange(callback: (schema: GraphQLSchema, supergraphSdl: string) => void): Unsubscriber { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a backwards-incompatible change (for TypeScript users) where existing Apollo Server won't be able to pass in the function it currently passes in. Though maybe I'm wrong due to how assignability works in TypeScript. Perhaps you could write a tiny test that shows that passing in a single-arg callback still compiles?
Hmm. OK, I played around and you're right that this won't break using old AS with new Gateway. But I think the other way around may be a challenge: passing a two-arg function to an old Gateway that expects one arg will not compile. And in fact that makes sense: if you use new AS with old Gateway you will have code in your callback that expects to get two string
s but the second one won't be there.
I suggest that we define this callback type as (schema: GraphQLSchema, supergraphSdl?: string) => void
, and in apollo-server you check if supergraphSdl is defined and blow up with some error about needing to upgrade @apollo/gateway
if it isn't there (or do some other behavior that is ok with no supergraphSdl).
Do you intend to eventually migrate the SchemaChangeCallback type? Leaving it around indefinitely with the wrong typing sounds confusing. Not sure exactly what the constraints here are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you allude to this in apollographql/apollo-server#5187
This is dependent on #738 . Specifically, when this PR lands, users will need to have access to a version of @apollo/gateway containing the referenced PR, or else their TS presumably won't compile (specifically, I believe they'll observe that ApolloGateway won't implement the interface GraphQLService, although I haven't checked this yet).
I see that that PR updates the SchemaChangeCallback type too.
I think you're right about what the problem will be. Making the second arg optional would help here. (Or making an entirely new callback type and onSchemaChangeWithSupergraphSdl entry point! I think second arg optional is better though.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm down with making the second positional argument in SchemaChangeCallback
optional, and having the check essentially be a runtime check instead of a compile-time check. We can give better messaging with a runtime check, and we do a fair number of runtime checks already at gateway start so it's not unexpected. I've updated apollographql/apollo-server#5187 to make the argument optional.
@glasser @trevor-scheer
If this sounds fine, let me know and I'll update the PR description and |
10bdc9e
to
e32bd44
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks right. Do be sure to update the PR description (and eventual squash commit) to describe what the latest version does, and the CHANGELOG as you mentioned in it.
Re the two comments below: mostly just take a glance to make sure your PR hasn't made anything worse (eg made it more likely that an error could lead to those fields getting out of sync with the actual schema), and perhaps add some TODO comments? I mean if you're inspired to introduce SchemaDerivedData I wouldn't complain but I'm sure you want to merge too.
gateway-js/src/index.ts
Outdated
@@ -389,10 +399,9 @@ export class ApolloGateway implements GraphQLService { | |||
throw e; | |||
} | |||
|
|||
this.schema = schema; | |||
// TODO(trevor): #580 redundant parse | |||
this.parsedSupergraphSdl = parse(supergraphSdl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: should this be part of updateWithSchemaAndNotify too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, looking at the code there's two things that make me a bit wary about moving it into updateWithSchemaAndNotify()
:
- Based on this block:
It looks like we parse early to avoid accidental state update (we could double-parse, but I think we've been trying to avoid redundant parses).
// TODO(trevor): #580 redundant parse // This may throw, so we'll calculate early (specifically before making any updates) // In the case that it throws, the gateway will: // * on initial load, throw the error // * on update, log the error and don't update const parsedSupergraphSdl = parse(result.supergraphSdl);
- We don't update
this.parsedSupergraphSdl
inupdateByComposition()
, which initially gave me the impression we only update it when we're handed a core supergraph schema directly and not when we get a service list. But looking atloadStatic()
, we do actually updatethis.parsedSupergraphSdl
when runningthis.createSchemaFromServiceList()
, so I'm not sure why we don't update it inupdateByComposition()
. Maybe @trevor-scheer might know more here?
((e && e.message) || e), | ||
); | ||
} | ||
this.updateWithSchemaAndNotify(schema, supergraphSdl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought: should the this.queryPlanStore.flush()
above be part of updateWithSchemaAndNotify too? It would seem you would want to do that as close as possible to changing the current schema. (Though I do feel like moving to more of a "schema derived data" that is swapped out at once like in ApolloServer would solve both of these thoughts.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I'm down to move it into updateWithSchemaAndNotify()
. We weren't doing it before in loadStatic()
, but that's probably because it's only called at initial schema load, so it just ends up being a no-op.
I agree that "schema derived data" would be nice in gateway, although I'll leave that to future PRs. (I've added a TODO about it.)
I've updated the PR description and the I won't merge this PR in until apollographql/apollo-server#5187 gets approved. (There's a somewhat lingering question above about |
4ca97d3
to
571c6de
Compare
571c6de
to
bd26ee1
Compare
Note that there will be a slight merge-conflict with #807 (shouldn't be hard to fix, but worth noting). |
bd26ee1
to
bdf77a3
Compare
…x bug where loadStatic() doesn't notify listeners
…d revert addition of optional argument and behavior change to loadStatic() for onSchemaChange()
…nd onSchemaLoadOrUpdate
bdf77a3
to
c80ac2b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a quick re-skim and this looks good!
#5187) To summarize, this PR: - Updates Apollo Server to support a new `schemaDidLoadOrUpdate` event hook. - Updates the Apollo Server schema reporting plugin to work when the server is configured with a gateway. Note that this PR is dependent on apollographql/federation#738 : - This PR has FIXMEs that require knowing the release version of gateway that lands the other PR. - To use schema reporting with gateways, users must use a gateway with at least the above release version. This PR specifically: - Changes `GatewayInterface`to have a new method `onSchemaLoadOrUpdate`, for registering listeners that are notified on both schema load and schema update, and for providing the core schema in addition to the API schema. - This method is implemented in apollographql/federation#738 . - The `onSchemaChange` method has been marked deprecated and changed to optional, to support its eventual removal from the gateway API. - Creates a new TS class `SchemaManager` for consolidating schema-tracking logic and abstracting away the gateway. This class specifically: - Acts as a source-of-truth for the schema and schema-derived data, updating it as needed. - Supports registering listeners for (and notifying listeners of) schema load/update events. - Is `async`-safe, i.e. it ensures linearizability for the TS class's public methods (provided `start()` is called and completes before `stop()`). - Adds a new `schemaDidLoadOrUpdate` event hook so that plugins can listen to schema load/update events. - `ApolloServerBase._start()` registers such event hooks as schema load/update event listeners for the `SchemaManager`. - Updates `ApolloServerPluginSchemaReporting` to use the new plugin event hook. - The hook, when called, creates a new `SchemaReporter` instance and starts it after stopping the old `SchemaReporter` instance (if it exists). - Removes some states and properties from `ServerState` that are no longer necessary due to `SchemaManager`. - The two `initialized` states have been consolidated, and the `invoking serverWillStart` state has been removed. - The property `schemaDerivedData` has effectively been replaced by `schemaManager`, while `loadedSchema` has been removed entirely. - Updates a test that was relying on a race condition between `start()` and `stop()` to resolve in a particular way to instead manually wait on `start()` before calling `stop()`. - Updates plugin docs, migration docs, and changelog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Th
For gateway schema reporting, we would like to know the (core) supergraph SDL when the schema is loaded or updates. To that end, this PR:
onSchemaLoadOrUpdate()
that registers listeners for schema loads/updates.onSchemaChange()
, this method ensures listeners are called during initial schema load.onSchemaChange()
would not notify listeners when the schema is loaded vialoadStatic()
, butonSchemaLoadOrUpdate()
does notify.onSchemaChange()
, this method provides listeners with both the API schema and the core supergraph SDL.onSchemaChange()
, this method provides listeners data via properties on a single argument object instead of multiple arguments.onSchemaChange()
in favor ofonSchemaLoadOrUpdate()
.updateWithSchemaAndNotify()
.gateway-js/src/index.ts
.gateway-js/CHANGELOG.md
.