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

chore(gateway): Simplify startup code paths #440

Merged
merged 19 commits into from
Jan 29, 2021

Conversation

trevor-scheer
Copy link
Member

In my attempts to implement CSDL fetching for managed gateway configurations, I found the complexity of our startup code to really inhibit my progress (and in turn, I'll need to add a bit more complexity for what I'm trying to accomplish).

After thinking through what is actually happening during startup (and wanting to simplify the code paths specifically loadServiceDefinitions), I was able to tease out the idea of static vs. dynamic configs and isolate those code paths a bit. Static === (local || csdl) configs where the gateway never updates.

Previously, we had loadServiceDefinitions overloaded to handle every type of config, but really it doesn't need to concern itself with the static cases. Those cases, I found, could actually be handled synchronously within the constructor!

The effect of these changes can be seen in the test updates that I had to make - the gateway was actually going through a full async update cycle even in the cases where that was totally unnecessary.

Copy link
Member

@glasser glasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this is a lot more clear than the old version.

gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Outdated Show resolved Hide resolved
gateway-js/src/index.ts Show resolved Hide resolved
@@ -727,13 +793,40 @@ export class ApolloGateway implements GraphQLService {
});
}

const canUseManagedConfig =
this.apolloConfig?.graphId && this.apolloConfig?.keyHash;
if (!canUseManagedConfig) {
throw new Error(
'When `serviceList` is not set, an Apollo configuration must be provided. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error isn't quite right, since there are other approaches like csdl

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added a test for this: 238fbec

Open to suggestions on rewording this, this one's kinda weird and, as demonstrated here, not the most obvious to keep up to date.

this.maybeWarnOnConflictingConfig();
}

await this.updateComposition();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be helpful to rename updateComposition and updateServiceDefinitions to contain "dynamic", to be more clear they are only part of the dynamic case? On the other hand maybe this is implicit in the concept of updating.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to this but the names will get to be on the "mouthful" side. I'm open to suggestions if you can think of something nice, otherwise will leave as is for now.

@glasser
Copy link
Member

glasser commented Jan 28, 2021

I do think there is something to be said for "any given field is either always or never assigned synchronously by the constructor" as kind of an abstract principle that this is going against. But I think practically speaking it's nice to be able to have things synchronously exist when possible.

@trevor-scheer
Copy link
Member Author

@glasser I think this was a great comment:

I do think there is something to be said for "any given field is either always or never assigned synchronously by the constructor" as kind of an abstract principle that this is going against. But I think practically speaking it's nice to be able to have things synchronously exist when possible.

Addressed via 525df37. Let me know if you meant something else, but this was my takeaway and I'm pretty happy with the result.

Copy link
Member

@glasser glasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically good, a few tiny suggestions?

({ schema, composedSdl } = this.createSchema(schemaConstructionOpts));
} catch (e) {
throw Error(
"A valid schema couldn't be composed. The following errors were found:\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this error appropriate in the CSDL case? It wasn't there before this commit and the wording is a bit off for it.

(Though I do wonder why we need this try/catch anyway rather than have createSchema throw a useful error in the first place? What particular error is this trying to annotate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct, this.createSchema flat out won't throw in the CSDL case so I can go back to splitting the cases via two ifs.

This message precedes a list of graphql errors. We could just let that bubble up and not annotate it at all, or add this annotation over there instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess I'm wondering if adding it more tightly would help. It's an error about "composing", it could be thrown in the compose function or right when that is called?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice change and additionally gives us better control over how the list of composition errors is displayed (GraphQLSchemaValidationError forced multiple new lines between each error in the printing).
d66d683

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I don't think I understand the implications of switching from GraphQLSchemaValidationError to just a plain Error (eg nothing checks error types when catching, right?) but trust that you do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a safe change (esp. since we were already just catching it and rethrowing as a plain Error).

There may have been some things to be careful of if it was public surface area / in use in other places, but not the case here.

const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged';
// Handles initial assignment of `this.schema`, `this.queryPlannerPointer`
isStaticConfig(this.config)
? this.loadStatic(this.config)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason that loadStatic takes config as an argument and loadDynamic reads it from this? I mean it's not bad for them to have different interfaces, but if it's actually important that they differ I don't see why.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadDynamic no longer reads from config at all, which is why the arg went away.

I prefer loadStatic to accept it as an argument since we can refine the type of config to StaticConfig on its way in, rather than do another isStaticConfig(this.config) once inside the function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see — loadDynamic calls functions which read from this.config but doesn't do so directly.

Actually, thinking about this makes me wonder if Experimental_UpdateServiceDefinitions should be changed to take a DynamicConfig instead of a GatewayConfig?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gateway-js/src/index.ts Outdated Show resolved Hide resolved
@@ -781,8 +781,7 @@ export class ApolloGateway implements GraphQLService {
graphId: this.apolloConfig!.graphId!,
apiKeyHash: this.apolloConfig!.keyHash!,
graphVariant: this.apolloConfig!.graphVariant,
federationVersion:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool!

({ schema, composedSdl } = this.createSchema(schemaConstructionOpts));
} catch (e) {
throw Error(
"A valid schema couldn't be composed. The following errors were found:\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I don't think I understand the implications of switching from GraphQLSchemaValidationError to just a plain Error (eg nothing checks error types when catching, right?) but trust that you do.

@trevor-scheer trevor-scheer merged commit 3b63c5d into main Jan 29, 2021
@trevor-scheer trevor-scheer deleted the trevor/simplify-gateway-startup branch January 29, 2021 01:31
glasser added a commit that referenced this pull request Aug 2, 2022
The point of apollo-server-caching is to provide an abstraction over
multiple cache backends. Gateway is not using that abstraction; it's
just using one particular implementation, which is a wrapper around an
old version of lru-cache.

As part of apollographql/apollo-server#6057
and apollographql/apollo-server#6719 we want
to remove dependencies on Apollo Server from Apollo Gateway. Technically
we don't really need to remove this dependency (since
apollo-server-caching doesn't depend on anything else in AS) but
apollo-server-caching won't be updated any more (in fact, even in AS3 it
has already been replaced by `@apollo/utils.keyvaluecache`), so let's do
it.

While we're at it, we make a few other improvements:
- Ever since #440, the queryPlanStore field is always set, so we can
  remove some conditionals around it.
- Instead of using the old lru-cache@6 wrapped by the
  apollo-server-caching package, we use the newer lru-cache@7 (which
  improves the algorithm internally and changes the names of methods a
  bit).
- The get and set methods on InMemoryLRUCache were only async because
  they implement the abstract KeyValueCache interface: the
  implementations didn't actually do anything async. So we no longer
  need to await them or include a giant comment about how we're not
  awaiting them.
glasser added a commit that referenced this pull request Aug 2, 2022
The point of apollo-server-caching is to provide an abstraction over
multiple cache backends. Gateway is not using that abstraction; it's
just using one particular implementation, which is a wrapper around an
old version of lru-cache.

As part of apollographql/apollo-server#6057
and apollographql/apollo-server#6719 we want
to remove dependencies on Apollo Server from Apollo Gateway. Technically
we don't really need to remove this dependency (since
apollo-server-caching doesn't depend on anything else in AS) but
apollo-server-caching won't be updated any more (in fact, even in AS3 it
has already been replaced by `@apollo/utils.keyvaluecache`), so let's do
it.

While we're at it, we make a few other improvements:
- Ever since #440, the queryPlanStore field is always set, so we can
  remove some conditionals around it.
- Instead of using the old lru-cache@6 wrapped by the
  apollo-server-caching package, we use the newer lru-cache@7 (which
  improves the algorithm internally and changes the names of methods a
  bit).
- The get and set methods on InMemoryLRUCache were only async because
  they implement the abstract KeyValueCache interface: the
  implementations didn't actually do anything async. So we no longer
  need to await them or include a giant comment about how we're not
  awaiting them.
glasser added a commit that referenced this pull request Aug 2, 2022
The point of apollo-server-caching is to provide an abstraction over
multiple cache backends. Gateway is not using that abstraction; it's
just using one particular implementation, which is a wrapper around an
old version of lru-cache.

As part of apollographql/apollo-server#6057
and apollographql/apollo-server#6719 we want
to remove dependencies on Apollo Server from Apollo Gateway. Technically
we don't really need to remove this dependency (since
apollo-server-caching doesn't depend on anything else in AS) but
apollo-server-caching won't be updated any more (in fact, even in AS3 it
has already been replaced by `@apollo/utils.keyvaluecache`), so let's do
it.

While we're at it, we make a few other improvements:
- Ever since #440, the queryPlanStore field is always set, so we can
  remove some conditionals around it.
- Instead of using the old lru-cache@6 wrapped by the
  apollo-server-caching package, we use the newer lru-cache@7 (which
  improves the algorithm internally and changes the names of methods a
  bit).
- The get and set methods on InMemoryLRUCache were only async because
  they implement the abstract KeyValueCache interface: the
  implementations didn't actually do anything async. So we no longer
  need to await them or include a giant comment about how we're not
  awaiting them.
glasser added a commit that referenced this pull request Aug 2, 2022
* gateway: remove dependency on apollo-server-caching (#2029)

The point of apollo-server-caching is to provide an abstraction over
multiple cache backends. Gateway is not using that abstraction; it's
just using one particular implementation, which is a wrapper around an
old version of lru-cache.

As part of apollographql/apollo-server#6057
and apollographql/apollo-server#6719 we want
to remove dependencies on Apollo Server from Apollo Gateway. Technically
we don't really need to remove this dependency (since
apollo-server-caching doesn't depend on anything else in AS) but
apollo-server-caching won't be updated any more (in fact, even in AS3 it
has already been replaced by `@apollo/utils.keyvaluecache`), so let's do
it.

While we're at it, we make a few other improvements:
- Ever since #440, the queryPlanStore field is always set, so we can
  remove some conditionals around it.
- Instead of using the old lru-cache@6 wrapped by the
  apollo-server-caching package, we use the newer lru-cache@7 (which
  improves the algorithm internally and changes the names of methods a
  bit).
- The get and set methods on InMemoryLRUCache were only async because
  they implement the abstract KeyValueCache interface: the
  implementations didn't actually do anything async. So we no longer
  need to await them or include a giant comment about how we're not
  awaiting them.

* gateway RemoteGraphQLDataSource: throw GraphQLError, not ApolloError (#2028)

This is part of
apollographql/apollo-server#6057 (which is
itself part of
apollographql/apollo-server#6719). We are
trying to break the dependency of Gateway on Server so that (among other
things) it is easier to have a single version of Gateway that works with
both the current AS3 and the upcoming AS4.

In AS4, we are removing the ApolloError class and its subclasses.
Instead, we will just use GraphQLError directly. See:

https://www.apollographql.com/docs/apollo-server/v4/migration#apolloerror
https://www.apollographql.com/docs/apollo-server/v4/migration#built-in-error-classes
apollographql/apollo-server#6355
apollographql/apollo-server#6705

This commit changes RemoteGraphQLDataSource to throw GraphQLError
instead of ApolloError. The `code` extension will still be the same.
(The `name` field of the thrown Error will no longer be eg
`AuthenticationError`, though; this does not affect the error as
serialized in GraphQL.)

This is technically slightly backwards-incompatible (eg, the method
errorFromResponse is public and now returns GraphQLError instead of the
tighter ApolloError) but this doesn't seem likely to affect many users.
We can adjust based on feedback if necessary.

* Adjust #2028 for [email protected] compatibility
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants