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

Add callback when fetching a supergraph from Apollo Uplink fails #1812

Merged
merged 38 commits into from
Jun 23, 2022

Conversation

benweatherman
Copy link
Contributor

@benweatherman benweatherman commented Apr 30, 2022

When polling Uplink fails, it's nice to have a fallback to load the supergraph from somewhere else.

This change exposes UplinkSupergraphManager (the artist formerly known as UplinkFetcher). It also adds config options for onFailureToFetchSupergraphSdlDuringInit and onFailureToFetchSupergraphSdlAfterInit that will be called if fetching from Uplink fails. It can return a string that will be used to update the supergraph schema. onFailureToFetchSupergraphSdlAfterInit can return null to signal the gateway should continue to use the existing schema.

Basic usage looks like the following:

import { promises } from 'fs';
import * as path from 'path';
import { ApolloServer } from 'apollo-server';
import { ApolloGateway, UplinkSupergraphManager } from '@apollo/gateway';

const gateway = new ApolloGateway({
  debug: true,
  supergraphSdl: new UplinkSupergraphManager({
    apiKey: process.env.APOLLO_KEY + "x", // Adding a "x" to artificially ensure we have an invalid key
    graphRef: process.env.APOLLO_GRAPH_REF,
    initialMaxRetries: 0, // Don't retry during initialization phase, so fallback is called sooner
    async onFailureToFetchSupergraphSdlDuringInit({ error, graphRef, logger }) {
      const filePath = path.resolve(__dirname, 'supergraph.graphql');
      logger.info(`Falling back to cached graph for ${graphRef} during init due to ${error}, loading from ${filePath}`);
      return promises.readFile(filePath, {encoding: 'utf8'});
    },
    async onFailureToFetchSupergraphSdlAfterInit({ error, graphRef, logger, fetchCount }) {
      const filePath = path.resolve(__dirname, 'supergraph.graphql');
      logger.info(`Falling back to cached graph for ${graphRef} after init due to ${error}, loading from ${filePath} (fetch count: ${fetchCount})`);
      return null;
    }
  }),
});
const server = new ApolloServer({ gateway });

server
  .listen()
  .then(({ url }) => {
    console.log(`🚀 Gateway ready at ${url}`);
  })
  .catch((err) => {
    console.error(err);
  });

Fixes #1801

TODO

  • Unit tests
  • Ensure good errors if callback provides invalid schema
  • Make it easier to use UplinkSupergraphManager defaults

Things that aren't here

  1. I started refactoring UplinkSupergraphManager into its own package so we could make big changes without needing to make a major version bump in the gateway. That requires making another package that holds just the types, so it can be used by the new uplink supergraph manager package and the gateway. That complicates the release slightly because we'd potentially want to version those separately (since the whole reason for breaking it out is to have the option of making breaking changes). It doesn't seem like the benefit is worth the effort and ongoing maintenance.
  2. I thought about making onFailureToUpdateSupergraphSdl a function on gateway (in line with onSchemaLoadOrUpdate), but that seemed like a larger refactor and can still be added in the future. That also has a slightly different mechanism than onFailureToFetchSupergraphSdl.

When polling Uplink fails, it's nice to have a fallback to load the supergraph from somewhere else.

The callback is called with `this === UplinkFetcher` and passes an `Error` object. The expected return value is supergraph schema text. I made the `UplinkFetcher.config` protected so the callback could read that info if needed. I'm very open to these being incorrect, so happy for any feedback on inputs/outputs (and anything else!)
@netlify
Copy link

netlify bot commented Apr 30, 2022

Deploy Preview for apollo-federation-docs canceled.

Name Link
🔨 Latest commit 0434ecd
🔍 Latest deploy log https://app.netlify.com/sites/apollo-federation-docs/deploys/62aa37cd18d21100099fec15

@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 30, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@benweatherman
Copy link
Contributor Author

Moved this back to a draft while I add some unit tests

@lennyburdette
Copy link
Contributor

lennyburdette commented May 2, 2022

What do you think about also providing a function for generating the default config values? All that code lives within the ApolloGateway constructor right now. Something like:

const gateway = new ApolloGateway({
  debug: true,
  supergraphSdl: new UplinkFetcher({
    ...uplinkFetcherDefaults(),
    async updateSupergraphSdlFailureCallback(this: UplinkFetcher, { error }: {error: Error}): Promise<string> {
      // ...
    },
  }),
});

@benweatherman
Copy link
Contributor Author

@lennyburdette great suggestion! I'll incorporate that as well.

@lennyburdette
Copy link
Contributor

any progress on this? i'd love to update the fallback demos to use this approach!

Rather than adding a `getUplinkFetcherConfig` function, I made it easier to use the `UplinkFetcher` constructor (basically made most of the config optional, so it's easy to keep defaults).

Also added a number of tests to check for managed config when creating an `UplinkFetcher` manually.
@benweatherman
Copy link
Contributor Author

benweatherman commented May 17, 2022

@lennyburdette Thanks for the ping! I just finished up some tests and refactoring how the config works. I started down the path of adding a uplinkFetcherDefaults function, but it seemed better for everyone to implement better defaults in the UplinkFetcher constructor. Thoughts on that approach?

Also, the idea came up of publishing UplinkFetcher as a separate package so we can iterate on features a bit faster if folks are gonna be using it directly. Any idea how useful that will be?

Finally, I could see this callback on failure being useful across other supergraph managers, so I'm wondering if we want a more generic mechanism than adding it to UplinkFetcher. This would probably be something similar to onSchemaLoadOrUpdate (possibly onSchemaLoadOrUpdateError).

@lennyburdette
Copy link
Contributor

Moving the default values to the UplinkFetcher itself makes sense! Hopefully makes the ApolloGateway class a little simpler too.

It can be a `public readonly` member. No need for funky `_` shenanigans.
It's good to keep it descriptive
Test for specific error messages when providing bad data from `onFailureToFetchSupergraphSdl`
Refactor the single callback into `onFailureToFetchSupergraphSdlDuringInit` and `onFailureToFetchSupergraphSdlAfterInit` to allow for returning `null` after init to signal re-using the existing supergraph.
Better code style all around
@benweatherman benweatherman self-assigned this Jun 9, 2022
@lleadbet
Copy link
Contributor

As a small comment/request- would it be possible to get the timestamp of the cachedSDL's last update for the onFailureToFetchSupergraphSdlAfterInit? To @lennyburdette's earlier point, serving outdated schemas would be bad, but if users could detect when it was last updated, they may be able to then confirm a new schema may be available via fallback.

That will allow users to set a low number of retries for gateway startup, but still have a "normal" amount of retries during regular operation
`minDelaySeconds` is being treated as optional, even though it's always returned. Updated a few uses of `SupergraphSdlUpdate` to be `Required<SupergraphSdlUpdate>`. I thought about updating `SupergraphSdlUpdate.minDelaySeconds` to be non-optional but that seemed like a bigger change and it was hard to trace where all the types were being used/exported.
This can give clients an idea of how long its been since the last successful fetch
@benweatherman
Copy link
Contributor Author

@lleadbet Thanks for the suggestion! I added a mostRecentSuccessfulFetchAt param. Does that give you the info you're looking for?

@benweatherman benweatherman merged commit 544cb4c into main Jun 23, 2022
@benweatherman benweatherman deleted the weatherman/uplink-fallback branch June 23, 2022 20:04
@benweatherman benweatherman changed the title Add callback option when polling Uplink fails Add callback when fetching a supergraph from Apollo Uplink fails Jun 23, 2022
@glasser
Copy link
Member

glasser commented Aug 8, 2022

Was it intentional that this PR changed the default log level to DEBUG?

@benweatherman
Copy link
Contributor Author

Nope, good catch! Documented and fixed as part of this issue #2047

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Uplink Fallback Function
7 participants