diff --git a/.changesets/fix_geal_request_completion.md b/.changesets/fix_geal_request_completion.md new file mode 100644 index 0000000000..1534afefaf --- /dev/null +++ b/.changesets/fix_geal_request_completion.md @@ -0,0 +1,12 @@ +### Execute the entire request pipeline if the client closed the connection ([Issue #4569](https://github.com/apollographql/router/issues/4569)), [Issue #4576](https://github.com/apollographql/router/issues/4576)), ([Issue #4589](https://github.com/apollographql/router/issues/4589)), ([Issue #4590](https://github.com/apollographql/router/issues/4590)), ([Issue #4611](https://github.com/apollographql/router/issues/4611)) + +The router is now making sure that the entire request handling pipeline is executed when the client closes the connection early, to let telemetry and any rhai scrit or coprocessor perform their tasks before canceling. Before that, when a client canceled a request, the entire execution was dropped and parts of the router, like telemetry, could not run properly. It now executes up to the first response event (in the case of subscription or `@defer` usage), adds a 499 status code to the response and skips the remaining subgraph requests. + +This change will report more requests to Studio and the configured telemetry, which will appear like a sudden increase in errors, because those failing requests were not reported before. To keep the previous behavior of immediately dropping execution for canceled requests, it is possible with the following option: + +```yaml +supergraph: + early_cancel: true +``` + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4770 \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index 18860939ce..b512d02af2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -258,7 +258,7 @@ commands: - run: name: Install CMake command: | - choco install cmake -y + choco install cmake.install -y echo 'export PATH="/c/Program Files/CMake/bin:$PATH"' >> "$BASH_ENV" exit $LASTEXITCODE - when: diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index 9ae088e975..45fbc8cf54 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,79 +1,102 @@ Release Checklist ================= -Nightly Releases ----------------- +## Table of Contents -As of the introduction of [PR #2409](https://github.com/apollographql/router/pull/2409), nightly releases are automatically built on a daily basis. This is accomplished automatically through use of a parameterized invocation of the [`nightly` workflow](https://github.com/apollographql/router/blob/HEAD/.circleci/config.yml#L704-L711) using [CircleCI's Scheduled Pipelines](https://circleci.com/docs/scheduled-pipelines/) feature. +- [Building a Release](#building-a-release) + - 👀 [Before you begin](#before-you-begin) + - [Starting a release PR](#starting-a-release-pr) - The release PR tracks a branch and gathers commits, allows changelog review. + - _(optional)_ [Cutting a pre-release](#cutting-a-pre-release) (i.e., release candidate, alpha or beta) - Near-final versions for testing. + - [Preparing the final release](#preparing-the-final-release) - Final version number bumps and final changelog preparation. + - [Finishing the release](#finishing-the-release) - Builds the final release, merges into `main`, reconcile `dev` trunk. + - Verifying the release (TODO) + - [Troubleshooting a release](#troubleshooting-a-release) - Something went wrong? +- [Nightly releases](#nightly-releases) -### One-off builds +## Building a Release -In the way the schedule is defined, nightly builds are done from the `dev` branch. However, the functionality that powers nightly builds can be used to also build from _any_ branch (including PRs) and produce a pre-release, "nightly style" build from any desired commit. +There are different types of releases: -This process can only be done by members of the Apollo Router `router` GitHub repository with contributor permissions on CircleCI. +- **General release** -To invoke a one-off `nightly` build: + These releases are typically identified with a `x.y.z` semver identifier. (e.g., `1.42.0`). +- **Pre-release** -1. Go to the CircleCI Pipelines view for this repository](https://app.circleci.com/pipelines/github/apollographql/router) -2. Click on the **"All Branches"** drop-down menu and choose a branch you'd like to build from. -3. Press the **"Trigger Pipeline"** button in the top-right of the navigation (to the left of the "Project Settings" button). -4. Expand the "Add Parameters" section. -5. Add one parameter using the following configuration: + Such a release could be a "release candidate", or an alpha/beta release and would use a `x.y.z-pre.w` identifier (e.g., `1.42.0-alpha.0`). +- **Nightly releases** - **Parameter type:** `boolean` - **Name:** `nightly` - **Value:** `true` -6. Press **"Trigger Pipeline"** -7. Wait a couple seconds for the pipeline to begin and show in the list. + These are special releases cut from a particular Git commit. Despite their name, they don't necessarily have to be done at a nightly interval. They are identified by an identifier of `v0.0.0-nightly-YYYYMMDD-COMMITHASH`. More details on these is found in the dedicated [Nightly releases] section. -To obtain the binary builds from the pipeline which was launched: + [Nightly releases]: #nightly-releases -> **Note** -> Built nightlies are only available on the Artifacts for a job within 30 days after the CircleCI pipeline that created them is finished. If you need them after this period, you will need to re-run the pipeline and wait for it to finish again. You can do this by clicking the "Rerun from start" option on the pipeline. +The process is **not fully automated**, but the release consists of copying and pasting commands that do all the work for you. Here's a high level understanding and some terminology, which will help so you can understand some key components: -1. Click on the workflow name: **`nightly`** of the newly launched pipeline. In the above steps, this is the pipeline that appeared after step 7. -2. Click on the job representing the system architecture you'd like to obtain the build binary for. For example, to get the macOS binary, click on `build_release-macos_build`. -3. If the job hasn't already finished successfully, **wait for the job to finish successfully**. -4. Click on the **Artifacts** tab. -5. Click on the link to the `.tar.gz` file to download the tarball of the build distribution. For example, you might click on a link called `artifacts/router-v0.0.0-nightly-20230119-abcd1234-x86_64-apple-darwin.tar.gz` for a macOS build done on the 19th of January 2023 from commit hash starting with `abcd1234`. +- Pull Requests + - There will be a total of 3 pull-requests involved: + - **a Release "Staging" PR**: this will merge **into `main`**. When it merges it will be a real merge commit and it **should NOT be squashed**. It starts off as a draft, and graduates to a "ready for review" PR once any pre-release versions are issued and after preparation is done. + - **a Release _Prep_ PR**: this will merge into the release PR _above_. It **SHOULD be squashed**. The release preparation PR is only done just before the final release and _**after** any prereleases_. + - **Reconciliation PR**: a PR that merges `main` back into `dev` after the final release is done. It will be a real merge commit and it **should NOT be squashed**. +- Peer Reviews + - The actual code being released will have been reviewed in other PRs. + - The "Release Prep" PR is reviewed -In addition, you will find `docker` and `helm` assets: - - [docker](https://github.com/apollographql/router/pkgs/container/nightly%2Frouter) - - [helm](https://github.com/apollographql/router/pkgs/container/helm-charts-nightly%2Frouter) +The examples below will use [the GitHub CLI (`gh`)](https://cli.github.com/) to simplify the steps. We can automate it further in the future, but feels like the right level of abstraction right now. -This is a list of the things that need to happen during a release. +### Before you begin -Build a Release ---------------- +#### Software requirements -Most of this will be executing some simple commands but here's a high level understanding and some terminology. There will be a total of 3 pull-requests involved: +Make sure you have the following software installed and available in your `PATH`. -- **a Release PR**: this will merge **into `main`**. It will be a real merge commit and it **should NOT be squashed**. -- **a Release _Prep_ PR**: this will merge into the release PR _above_. It **SHOULD be squashed**. -- **Reconciliation PR**: a PR that merges `main` back into `dev`. It will be a real merge commit and it **should NOT be squashed**. + - `gh`: [The GitHub CLI](https://cli.github.com/) + - `cargo`: [Cargo & Rust Installation](https://doc.rust-lang.org/cargo/getting-started/installation.html) + - `helm-docs`: see + - `cargo-about`: install with `cargo install --locked cargo-about` + - `cargo-deny`: install with `cargo install --locked cargo-deny` + - `set-version` from `cargo-edit`: `cargo install --locked cargo-edit` -The examples below will use [the GitHub CLI (`gh`)](https://cli.github.com/) to simplify the steps. We can automate it further in the future, but feels like the right level of abstraction right now. +#### Pick a version -A release can be cut from any branch, but we assume you'll be doing it from `dev`. If you're just doing a release candidate, you can skip merging it back into `main`. +This project uses [Semantic Versioning 2.0.0](https://semver.org/). When releasing, analyze the existing changes in the [`.changesets/`](./.changesets) directory to pick the right next version: -1. Make sure you have `cargo` installed on your machine and in your `PATH`. You also need: - - `helm-docs`: see - - `cargo-about`: `cargo install --locked cargo-about` - - `cargo-deny`: `cargo install --locked cargo-deny` - - `set-version` from `cargo-edit`: `cargo install --locked cargo-edit` -2. Pick the version number you are going to release. This project uses [Semantic Versioning 2.0.0](https://semver.org/), so analyze the existing changes in the `.changesets/` directory to pick the right next version. (e.g., If there are `feat_` changes, it must be a minor version bump. If there are `breaking_` changes, it must be a _major_ version bump). **Do not release a major version without explicit agreement from core team members**. -3. Checkout the branch you want to cut from. Typically, this is `dev`, but you could do this from another branch as well. +- If there are `feat_` changes, it must be a _semver-minor_ version bump. +- If there are `breaking_` changes, it must be a _semver-major_ version bump. **Do not release a major version without explicit agreement from core team members**. +- In all other cases, you can release a _semver-patch_ version. + +> **Note** +> The full details of the `.changesets/` file-prefix convention can be found [its README](.changesets/README.md#conventions-used-in-this-changesets-directory). + +### Starting a release PR + +Creating a release PR is the first step of starting a release, whether there will be pre-releases or not. About a release PR: + +* A release PR is based on a release branch and a release branch gathers all the commits for a release. +* The release PR merges into `main` at the time that the release becomes official. +* A release can be started from any branch or commit, but it is almost always started from `dev` as that is the main development trunk of the Router. +* The release PR is in a draft mode until after the preparation PR has been merged into it. + +Start following the steps below to start a release PR. The process is **not fully automated**, but largely consists of copying and pasting commands that do all the work for you. The descriptions above each command explain what the command aims to do. + +1. Make sure you have all the [Software Requirements](#software-requirements) above fulfilled. + +2. Ensure you have decided the version using [Pick a version](#pick-a-version). + +3. Checkout the branch or commit you want to cut from. Typically, this is `dev`, but you could do this from another branch as well: ``` git checkout dev ``` -4. We'll set some environment variables for steps that follow this, to simplify copy and pasting. Be sure to customize these for your own conditions, and **set the version you picked in the above step** as `APOLLO_ROUTER_RELEASE_VERSION`: +4. We'll set some environment variables for steps that follow this, which will enable copying-and-pasting subsequent steps. Customize these for your own conditions, **set the version you picked in the above step** as `APOLLO_ROUTER_RELEASE_VERSION`, and then paste into your terminal (press enter to complete it): + + > **Note** + > You should **not** fill in `APOLLO_ROUTER_PRERELEASE_SUFFIX` at this time. Visit [Cutting a pre-release](#cutting-a-pre-release) after opening the original release PR. ``` - APOLLO_ROUTER_RELEASE_VERSION=#.#.# + APOLLO_ROUTER_RELEASE_VERSION="#.#.#" # Set me! APOLLO_ROUTER_RELEASE_GIT_ORIGIN=origin APOLLO_ROUTER_RELEASE_GITHUB_REPO=apollographql/router + APOLLO_ROUTER_PRERELEASE_SUFFIX="" # Intentionally blank. ``` 5. Make sure you have the latest from the remote before releasing, ensuring you're using the right remote! @@ -82,27 +105,142 @@ A release can be cut from any branch, but we assume you'll be doing it from `dev git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" ``` -6. Create a new branch `#.#.#`. (The `#.#.#` values should be this release's version, and it is perfectly acceptable to use prerelease semantics, e.g., a branch named `1.5.3-rc.9`). To do this using the environment variable we just set, we'll just run the following from the same terminal: +6. Create a new branch `#.#.#` from the current branch which will act as the release branch. (The `#.#.#` values should be this release's version. To do this using the environment variable we just set, we'll just run the following from the same terminal: ``` git checkout -b "${APOLLO_ROUTER_RELEASE_VERSION}" ``` + 7. Push this new branch to the appropriate remote. We will open a PR for it **later**, but this will be the **base** for the PR created in the next step). (And `--set-upstream` will of course track this locally. This is commonly abbreviated as `-u`.) ``` git push --set-upstream "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -8. Create _another_ new branch called `prep-#.#.#` off of `#.#.#`. This branch will be used for bumping version numbers and getting review on the changelog. We'll do this using the same environment variable, so you can just run: +8. Now, open a draft PR with a small boilerplate header from the branch which was just pushed: ``` - git checkout -b "prep-${APOLLO_ROUTER_RELEASE_VERSION}" + cat < **Note** + > **This particular PR must be true-merged to \`main\`.** + + * This PR is only ready to review when it is marked as "Ready for Review". It represents the merge to the \`main\` branch of an upcoming release (version number in the title). + * It will act as a staging branch until we are ready to finalize the release. + * We may cut any number of alpha and release candidate (RC) versions off this branch prior to formalizing it. + * This PR is **primarily a merge commit**, so reviewing every individual commit shown below is **not necessary** since those have been reviewed in their own PR. However, things important to review on this PR **once it's marked "Ready for Review"**: + - Does this PR target the right branch? (usually, \`main\`) + - Are the appropriate **version bumps** and **release note edits** in the end of the commit list (or within the last few commits). In other words, "Did the 'release prep' PR actually land on this branch?" + - If those things look good, this PR is good to merge! + EOM ``` -9. On this new `prep-#.#.#` branch, run the release automation script using this command to use the environment variable set previously: +### Cutting a pre-release - > **Note** - > For this command, `GITHUB_TOKEN` is **not used**, but it is still _required_ at the moment, so it's set here to `prep`. This is a bug in the releasing script that needs to be changed. +1. Make sure you have all the [Software Requirements](#software-requirements) above fulfilled. + +2. Be aware of the version you are cutting a pre-release for. This would have been picked during the initial [Starting a release PR](#starting-a-release-pr) step. + +3. Select a pre-release suffix for the above version. This could be `-alpha.0`, `-rc.4` or whatever is appropriate. Most commonly, we cut `-rc.x` releases right before the final release. Release candidates should have minimal new substantial changes and only changes that are necessary to secure the release. + +4. We'll set some environment variables for steps that follow this, which will enable copying-and-pasting subsequent steps. **Customize these for your own conditions**: + + - Set the version from step 2 as `APOLLO_ROUTER_RELEASE_VERSION`; and + - Set the pre-release suffix from step 3 as `APOLLO_ROUTER_PRERELEASE_SUFFIX` + + ``` + APOLLO_ROUTER_RELEASE_VERSION="#.#.#" # Set me! + APOLLO_ROUTER_RELEASE_GIT_ORIGIN=origin + APOLLO_ROUTER_RELEASE_GITHUB_REPO=apollographql/router + APOLLO_ROUTER_PRERELEASE_SUFFIX="-word.#" # Set me! + ``` + + After editing, paste the resulting block into your terminal and press _Return_ to activate them. + +5. Change your local branch back to the _non-_prep branch, pull any changes you (or others) may have added on GitHub : + + ``` + git checkout "${APOLLO_ROUTER_RELEASE_VERSION}" && \ + git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" + ``` + +6. Run the release automation script using this command to use the environment variable set previously: + + ``` + cargo xtask release prepare "${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" + ``` + + Running this command will: + + - Bump the necessary versions to the version specified, including those in the documentation. + - Run our compliance checks and update the `licenses.html` file as appropriate. + - Ensure we're not using any incompatible licenses in the release. + + Currently, it will also do one step which we will **immediately undo** in the next step, since it is not desireable for pre-release versions: + + - Migrate the current set of `/.changesets/*.md` files into `/CHANGELOG.md` using the version specified. + +7. Revert the changes to the `CHANGELOG.md` made in the last step since we don't finalize the changelog from the `.changesets` until the final release is prepared. (This really could be replaced with a `--skip-changesets` flag.) + + ``` + git checkout -- .changesets/ CHANGELOG.md + ``` + +8. Now, review and stage he changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). + + ``` + git add -up . + ``` + +9. Now commit those changes locally, using a brief message: + + ``` + git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" + ``` + +10. Push this commit up to the existing release PR: + + ``` + git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" + ``` + +10. Git tag & push the pre-release: + + This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. + + ``` + git tag -a "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" -m "${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" && \ + git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "v${APOLLO_ROUTER_RELEASE_VERSION}${APOLLO_ROUTER_PRERELEASE_SUFFIX}" + ``` + +### Preparing the final release + +1. Make sure you have all the [Software Requirements](#software-requirements) above fulfilled. + +2. Ensure you have decided the version using [Pick a version](#pick-a-version). + +3. We'll set some environment variables for steps that follow this, which will enable copying-and-pasting subsequent steps. Customize these for your own conditions, **set the version you picked in the above step** as `APOLLO_ROUTER_RELEASE_VERSION`, and then paste into your terminal (press enter to complete it): + + ``` + APOLLO_ROUTER_RELEASE_VERSION="#.#.#" # Set me! + APOLLO_ROUTER_RELEASE_GIT_ORIGIN=origin + APOLLO_ROUTER_RELEASE_GITHUB_REPO=apollographql/router + APOLLO_ROUTER_PRERELEASE_SUFFIX="" # Intentionally blank. + ``` + +4. Change your local branch back to the _non-_prep branch, pull any changes you (or others) may have added on GitHub : + + ``` + git checkout "${APOLLO_ROUTER_RELEASE_VERSION}" && \ + git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" + ``` + +5. Create a new branch called `prep-#.#.#` off of `#.#.#`. This branch will be used for bumping version numbers and getting final review on the changelog. We'll do this using the environment variables, so you can just run: + + ``` + git checkout -b "prep-${APOLLO_ROUTER_RELEASE_VERSION}" + ``` + +6. On this new `prep-#.#.#` branch, run the release automation script using this command to use the environment variable set previously: ``` cargo xtask release prepare $APOLLO_ROUTER_RELEASE_VERSION @@ -115,7 +253,7 @@ A release can be cut from any branch, but we assume you'll be doing it from `dev - Run our compliance checks and update the `licenses.html` file as appropriate. - Ensure we're not using any incompatible licenses in the release. -10. **MANUALLY CHECK AND UPDATE** the `federation-version-support.mdx` to make sure it shows the version of Federation which is included in the `router-bridge` that ships with this version of Router. This can be obtained by looking at the version of `router-bridge` in `apollo-router/Cargo.toml` and taking the number after the `+` (e.g., `router-bridge@0.2.0+v2.4.3` means Federation v2.4.3). +7. **MANUALLY CHECK AND UPDATE** the `federation-version-support.mdx` to make sure it shows the version of Federation which is included in the `router-bridge` that ships with this version of Router. This can be obtained by looking at the version of `router-bridge` in `apollo-router/Cargo.toml` and taking the number after the `+` (e.g., `router-bridge@0.2.0+v2.4.3` means Federation v2.4.3). 11. Now, review and stage he changes produced by the previous step. This is most safely done using the `--patch` (or `-p`) flag to `git add` (`-u` ignores untracked files). @@ -129,9 +267,9 @@ A release can be cut from any branch, but we assume you'll be doing it from `dev git commit -m "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}" ``` -13. (Optional) Make local edits to the newly rendered `CHANGELOG.md` entries to do some initial editoral. +13. _**(Optional)**_ Make local edits to the newly rendered `CHANGELOG.md` entries to do some initial editoral. - These things should typically be resolved earlier in the review process, but need to be double checked: + These things should have *ALWAYS* been resolved earlier in the review process of the PRs that introduced the changes, but they must be double checked: - There are no breaking changes. - Entries are in categories (e.g., Fixes vs Features) that make sense. @@ -195,56 +333,47 @@ A release can be cut from any branch, but we assume you'll be doing it from `dev echo "${apollo_prep_release_header}\n${apollo_prep_release_notes}" | gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr create -B "${APOLLO_ROUTER_RELEASE_VERSION}" --title "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}" --body-file - ``` -18. Use the `gh` CLI to enable **auto-squash** (**_NOT_** auto-**_merge_**) on the PR you just opened: - - ``` - gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr merge --squash --body "" -t "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}" --auto "prep-${APOLLO_ROUTER_RELEASE_VERSION}" - ``` +18. 🗣️ **Solicit feedback from the Router team on the prep PR** -19. 🗣️ **Solicit feedback from the Router team on the prep PR** + Once approved, you can proceed with [Finishing the release](#finishing-the-release). - Once approved, the PR will squash-merge itself into the next branch. +### Finishing the release -20. After the PR has auto-merged, change your local branch back to the _non-_prep branch, pull any changes you (or others) may have added on GitHub : +1. Make sure you have all the [Software Requirements](#software-requirements) above fulfilled. - ``` - git checkout "${APOLLO_ROUTER_RELEASE_VERSION}" && \ - git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" - ``` +2. Be aware of the version you are finalizing. This would have been picked during the initial [Starting a release PR](#starting-a-release-pr) step. -20. Now, from your local final release branch, open the PR from the branch the prep PR already merged into: +3. We'll set some environment variables for steps that follow this, which will enable copying-and-pasting subsequent steps. Customize these for your own conditions, **set the version you picked in the above step** as `APOLLO_ROUTER_RELEASE_VERSION`, and then paste into your terminal (press enter to complete it): - ``` - apollo_release_pr_header="$( - cat < **Note** - > **This particular PR should be true-merged to \`main\`.** - - This PR represents the merge to \`main\` of the v${APOLLO_ROUTER_RELEASE_VERSION} release. + ``` + APOLLO_ROUTER_RELEASE_VERSION="#.#.#" # Set me! + APOLLO_ROUTER_RELEASE_GIT_ORIGIN=origin + APOLLO_ROUTER_RELEASE_GITHUB_REPO=apollographql/router + APOLLO_ROUTER_PRERELEASE_SUFFIX="" # Intentionally blank. + ``` - This PR is **primarily a merge commit**, so reviewing every individual commit shown below is **not necessary** since those have been reviewed in their own PR. +4. Use the `gh` CLI to **squash** (**_NOT true-merge_**) on the prep PR opened previously: - **However!** Some things to review on this PR: + ``` + gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr merge --squash --body "" -t "prep release: v${APOLLO_ROUTER_RELEASE_VERSION}" "prep-${APOLLO_ROUTER_RELEASE_VERSION}" + ``` - - Does this PR target the right branch? (usually, \`main\`) - - Are the appropriate **version bumps** and **release note edits** in the end of the commit list (or within the last few commits). In other words, "Did the 'release prep' PR actually land on this branch?" +5. After the prep PR has squash-merged into the release PR, change your local branch back to release branch, pull any changes you (or others) may have added on GitHub, so you have them locally: - If those things look good, this PR is good to merge. - EOM - )" - echo "${apollo_release_pr_header}" | gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr create -B "main" --title "release: v${APOLLO_ROUTER_RELEASE_VERSION}" --body-file - + ``` + git checkout "${APOLLO_ROUTER_RELEASE_VERSION}" && \ + git pull "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -21. Use the `gh` CLI to enable **auto-merge** (**_NOT_** auto-**_squash_**): +6. Use the `gh` CLI to enable **auto-merge** (**_NOT_** auto-**_squash_**): ``` gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr merge --merge --body "" -t "release: v${APOLLO_ROUTER_RELEASE_VERSION}" --auto "${APOLLO_ROUTER_RELEASE_VERSION}" ``` -22. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `main`** +7. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `main`** -23. After the PR has merged to `main`, pull `main` to your local terminal, and Git tag & push the release: +8. After the PR has merged to `main`, pull `main` to your local terminal, and Git tag & push the release: This process will kick off the bulk of the release process on CircleCI, including building each architecture on its own infrastructure and notarizing the macOS binary. @@ -255,103 +384,114 @@ A release can be cut from any branch, but we assume you'll be doing it from `dev git push "${APOLLO_ROUTER_RELEASE_GIT_ORIGIN}" "v${APOLLO_ROUTER_RELEASE_VERSION}" ``` -24. Open a PR that reconciles `dev` (Make sure to merge this reconciliation PR back to dev, do not squash or rebase): +9. Open a PR that reconciles `dev` (Make sure to merge this reconciliation PR back to dev, **do not squash or rebase**): ``` gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr create --title "Reconcile \`dev\` after merge to \`main\` for v${APOLLO_ROUTER_RELEASE_VERSION}" -B dev -H main --body "Follow-up to the v${APOLLO_ROUTER_RELEASE_VERSION} being officially released, bringing version bumps and changelog updates into the \`dev\` branch." ``` -25. 👀 Follow along with the process by [going to CircleCI for the repository](https://app.circleci.com/pipelines/github/apollographql/router) and clicking on `release` for the Git tag that appears at the top of the list. **Wait for `publish_github_release` to finish on this job before continuing.** +10. Mark the PR to **auto-merge NOT auto-squash** using the URL that is output from the previous command + + ``` + APOLLO_RECONCILE_PR_URL=$(gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr list --state open --base dev --head main --json url --jq '.[-1] | .url') + test -n "${APOLLO_RECONCILE_PR_URL}" && \ + gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" pr merge "${APOLLO_RECONCILE_PR_URL}" + ``` + -26. After the CI job has finished for the tag, re-run the `perl` command from Step 15, which will regenerate the `this_release.md` with changes that happened in the release review. +11. 🗣️ **Solicit approval from the Router team, wait for the PR to pass CI and auto-merge into `dev`** -27. Change the links from `[@username](https://github.com/username)` to `@username` (TODO: Write more `perl` here. 😄) +12. 👀 Follow along with the process by [going to CircleCI for the repository](https://app.circleci.com/pipelines/github/apollographql/router) and clicking on `release` for the Git tag that appears at the top of the list. - This ensures that contribution credit is clearly displayed using the user avatars on the GitHub Releases page when the notes are published in the next step. +13. ⚠️ **Wait for `publish_github_release` on CircleCI to finish on this job before continuing.** ⚠️ -28. Update the release notes on the now-published [GitHub Releases](https://github.com/apollographql/router/releases) (this needs to be moved to CI, but requires `this_release.md` which we created earlier): + You should expect this will take at least 30 minutes. + +14. Re-create the file you may have previously created called `this_release.md` just to make sure its up to date after final edits from review: ``` - gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" release edit v"${APOLLO_ROUTER_RELEASE_VERSION}" -F ./this_release.md + perl -0777 \ + -sne 'print "$1\n" if m{ + (?:\#\s # Look for H1 Markdown (line starting with "# ") + \[v?\Q$version\E\] # ...followed by [$version] (optionally with a "v") + # since some versions had that in the past. + \s.*?\n$) # ... then "space" until the end of the line. + \s* # Ignore PRE-entry-whitespace + (.*?) # Capture the ACTUAL body of the release. But do it + # in a non-greedy way, leading us to stop when we + # reach the next version boundary/heading. + \s* # Ignore POST-entry-whitespace + (?=^\#\s\[[^\]]+\]\s) # Once again, look for a version boundary. This is + # the same bit at the start, just on one line. + }msx' -- \ + -version="${APOLLO_ROUTER_RELEASE_VERSION}" \ + CHANGELOG.md > this_release.md ``` -29. Publish the Crate from your local computer from the `main` branch (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): +15. Change the links in `this_release.md` from `[@username](https://github.com/username)` to `@username` in order to facilitate the correct "Contributorship" attribution on the final GitHub release. ``` - cargo publish -p apollo-router + perl -pi -e 's/\[@([^\]]+)\]\([^)]+\)/@\1/g' this_release.md ``` -30. (Optional) To have a "social banner" for this release, run [this `htmlq` command](https://crates.io/crates/htmlq) (`cargo install htmlq`, or on MacOS `brew install htmlq`; its `jq` for HTML), open the link it produces, copy the image to your clipboard: +16. Update the release notes on the now-published [GitHub Releases](https://github.com/apollographql/router/releases) (this needs to be moved to CI, but requires `this_release.md` which we just created): ``` - curl -s "https://github.com/apollographql/router/releases/tag/v${APOLLO_ROUTER_RELEASE_VERSION}" | htmlq 'meta[property="og:image"]' --attribute content + gh --repo "${APOLLO_ROUTER_RELEASE_GITHUB_REPO}" release edit v"${APOLLO_ROUTER_RELEASE_VERSION}" -F ./this_release.md ``` -### prep PR Review +17. Finally, publish the Crate from your local computer from the `main` branch (this also needs to be moved to CI, but requires changing the release containers to be Rust-enabled and to restore the caches): -Most review comments for the prep PR will be about the changelog. Once the prep PR is finalized and approved: + ``` + cargo publish -p apollo-router + ``` -1. Always use `Squash and Merge` GitHub button. +18. (Optional) To have a "social banner" for this release, run [this `htmlq` command](https://crates.io/crates/htmlq) (`cargo install htmlq`, or on MacOS `brew install htmlq`; its `jq` for HTML), open the link it produces, copy the image to your clipboard: -### Tag and build release + ``` + curl -s "https://github.com/apollographql/router/releases/tag/v${APOLLO_ROUTER_RELEASE_VERSION}" | htmlq 'meta[property="og:image"]' --attribute content + ``` -This part of the release process is handled by CircleCI, and our binaries are -distributed as GitHub Releases. When you push a version tag, it kicks off a -workflow that checks out the tag, builds release binaries for multiple -platforms, and creates a new GitHub release for that tag. +## Nightly Releases -1. Wait for tests to pass. -2. Have your PR merged to `main`. -3. Once merged, run `git checkout main` and `git pull`. -4. Sync your local tags with the remote tags by running - `git tag -d $(git tag) && git fetch --tags` -5. Tag the commit by running either `git tag -a v#.#.# -m "#.#.#"` (release), - or `git tag -a v#.#.#-rc.# -m "#.#.#-rc.#"` (release candidate) -6. Run `git push --tags`. -7. Wait for CI to pass. +As of the introduction of [PR #2409](https://github.com/apollographql/router/pull/2409), nightly releases are automatically built on a daily basis. This is accomplished automatically through use of a parameterized invocation of the [`nightly` workflow](https://github.com/apollographql/router/blob/HEAD/.circleci/config.yml#L704-L711) using [CircleCI's Scheduled Pipelines](https://circleci.com/docs/scheduled-pipelines/) feature. -### Edit the release +### One-off builds -After CI builds the release binaries, a new release will appear on the -[releases page](https://github.com/apollographql/router/releases), click -`Edit`, update the release notes, and save the changes to the release. +In the way the schedule is defined, nightly builds are done from the `dev` branch. However, the functionality that powers nightly builds can be used to also build from _any_ branch (including PRs) and produce a pre-release, "nightly style" build from any desired commit. -#### If this is a stable release (not a release candidate) +This process can only be done by members of the Apollo Router `router` GitHub repository with contributor permissions on CircleCI. -1. Paste the current release notes from `NEXT_CHANGELOG.md` into the release body. -2. Reset the content of `NEXT_CHANGELOG.md`. +To invoke a one-off `nightly` build: -#### If this is a release candidate +1. Go to the CircleCI Pipelines view for this repository](https://app.circleci.com/pipelines/github/apollographql/router) +2. Click on the **"All Branches"** drop-down menu and choose a branch you'd like to build from. +3. Press the **"Trigger Pipeline"** button in the top-right of the navigation (to the left of the "Project Settings" button). +4. Expand the "Add Parameters" section. +5. Add one parameter using the following configuration: -1. CI should already mark the release as a `pre-release`. Double check that - it's listed as a pre-release on the release's `Edit` page. -2. If this is a new rc (rc.0), paste testing instructions into the release - notes. -3. If this is a rc.1 or later, the old release candidate testing instructions - should be moved to the latest release candidate testing instructions, and - replaced with the following message: + **Parameter type:** `boolean` + **Name:** `nightly` + **Value:** `true` +6. Press **"Trigger Pipeline"** +7. Wait a couple seconds for the pipeline to begin and show in the list. - ```markdown - This beta release is now out of date. If you previously installed this - release, you should reinstall and see what's changed in the latest - [release](https://github.com/apollographql/router/releases). - ``` +To obtain the binary builds from the pipeline which was launched: - The new release candidate should then include updated testing instructions - with a small changelog at the top to get folks who installed the old - release candidate up to speed. +> **Note** +> Built nightlies are only available on the Artifacts for a job within 30 days after the CircleCI pipeline that created them is finished. If you need them after this period, you will need to re-run the pipeline and wait for it to finish again. You can do this by clicking the "Rerun from start" option on the pipeline. -### Publish the release to Crates.io +1. Click on the workflow name: **`nightly`** of the newly launched pipeline. In the above steps, this is the pipeline that appeared after step 7. +2. Click on the job representing the system architecture you'd like to obtain the build binary for. For example, to get the macOS binary, click on `build_release-macos_build`. +3. If the job hasn't already finished successfully, **wait for the job to finish successfully**. +4. Click on the **Artifacts** tab. +5. Click on the link to the `.tar.gz` file to download the tarball of the build distribution. For example, you might click on a link called `artifacts/router-v0.0.0-nightly-20230119-abcd1234-x86_64-apple-darwin.tar.gz` for a macOS build done on the 19th of January 2023 from commit hash starting with `abcd1234`. -0. **To perform these steps, you'll need access credentials which allow you publishing to Crates.io.** -1. Make sure you are on the Git tag you have published and pushed in the previous step by running `git checkout v#.#.#` (release) or `git checkout v#.#.#-rc.#` (release candidate). (You are probably still on this commit) -2. Change into the `apollo-router/` directory at the root of the repository. -3. Make sure that the `README.md` in this directory is up to date with any necessary or relevant changes. It will be published as the crates README on Crates.io. -4. Run `cargo publish --dry-run` if you'd like to smoke test things -5. Do the real publish with `cargo publish`. +In addition, you will find `docker` and `helm` assets: + - [docker](https://github.com/apollographql/router/pkgs/container/nightly%2Frouter) + - [helm](https://github.com/apollographql/router/pkgs/container/helm-charts-nightly%2Frouter) -Troubleshooting a release -------------------------- +## Troubleshooting a release Mistakes happen. Most of these release steps are recoverable if you mess up. diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index c59842076e..c7a9a9b3b8 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -37,6 +37,8 @@ use tower::ServiceBuilder; use tower::ServiceExt; use tower_http::decompression::DecompressionBody; use tower_http::trace::TraceLayer; +use tracing::instrument::WithSubscriber; +use tracing::Instrument; use super::listeners::ensure_endpoints_consistency; use super::listeners::ensure_listenaddrs_consistency; @@ -64,6 +66,7 @@ use crate::services::router; use crate::uplink::license_enforcement::LicenseState; use crate::uplink::license_enforcement::APOLLO_ROUTER_LICENSE_EXPIRED; use crate::uplink::license_enforcement::LICENSE_EXPIRED_SHORT_MESSAGE; +use crate::Context; static ACTIVE_SESSION_COUNT: AtomicU64 = AtomicU64::new(0); @@ -549,16 +552,17 @@ pub(super) fn main_router( where RF: RouterFactory, { + let early_cancel = configuration.supergraph.early_cancel; let mut router = Router::new().route( &configuration.supergraph.sanitized_path(), get({ move |Extension(service): Extension, request: Request>| { - handle_graphql(service.create().boxed(), request) + handle_graphql(service.create().boxed(), early_cancel, request) } }) .post({ move |Extension(service): Extension, request: Request>| { - handle_graphql(service.create().boxed(), request) + handle_graphql(service.create().boxed(), early_cancel, request) } }), ); @@ -569,13 +573,13 @@ where get({ move |Extension(service): Extension, request: Request>| { - handle_graphql(service.create().boxed(), request) + handle_graphql(service.create().boxed(), early_cancel, request) } }) .post({ move |Extension(service): Extension, request: Request>| { - handle_graphql(service.create().boxed(), request) + handle_graphql(service.create().boxed(), early_cancel, request) } }), ); @@ -586,6 +590,7 @@ where async fn handle_graphql( service: router::BoxService, + early_cancel: bool, http_request: Request>, ) -> impl IntoResponse { let _guard = SessionCountGuard::start(); @@ -602,7 +607,33 @@ async fn handle_graphql( .get(ACCEPT_ENCODING) .cloned(); - let res = service.oneshot(request).await; + let res = if early_cancel { + service.oneshot(request).await + } else { + // to make sure we can record request handling when the client closes the connection prematurely, + // we execute the request in a separate task that will run until we get the first response, which + // means it went through the entire pipeline at least once (not looking at deferred responses or + // subscription events). This is a bit wasteful, so to avoid unneeded subgraph calls, we insert in + // the context a flag to indicate that the request is canceled and subgraph calls should not be made + let mut cancel_handler = CancelHandler::new(&context); + let task = service + .oneshot(request) + .with_current_subscriber() + .in_current_span(); + let res = match tokio::task::spawn(task).await { + Ok(res) => res, + Err(_e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "router service call failed", + ) + .into_response(); + } + }; + cancel_handler.on_response(); + res + }; + let dur = context.busy_time(); let processing_seconds = dur.as_secs_f64(); @@ -654,12 +685,48 @@ async fn handle_graphql( } } +struct CancelHandler<'a> { + context: &'a Context, + got_first_response: bool, + span: tracing::Span, +} + +impl<'a> CancelHandler<'a> { + fn new(context: &'a Context) -> Self { + CancelHandler { + context, + got_first_response: false, + span: tracing::Span::current(), + } + } + + fn on_response(&mut self) { + self.got_first_response = true; + } +} + +impl<'a> Drop for CancelHandler<'a> { + fn drop(&mut self) { + if !self.got_first_response { + self.span + .in_scope(|| tracing::error!("broken pipe: the client closed the connection")); + self.context.extensions().lock().insert(CanceledRequest); + } + } +} + +pub(crate) struct CanceledRequest; + #[cfg(test)] mod tests { use std::str::FromStr; - use super::*; + use http::header::ACCEPT; + use http::header::CONTENT_TYPE; + use tower::Service; + use super::*; + use crate::assert_snapshot_subscriber; #[test] fn test_span_mode_default() { let config = @@ -687,4 +754,35 @@ mod tests { let mode = span_mode(&config); assert_eq!(mode, SpanMode::Deprecated); } + + #[tokio::test] + async fn request_cancel() { + let mut http_router = crate::TestHarness::builder() + .schema(include_str!("../testdata/supergraph.graphql")) + .build_http_service() + .await + .unwrap(); + + async { + let _res = tokio::time::timeout( + std::time::Duration::from_micros(100), + http_router.call( + http::Request::builder() + .method("POST") + .uri("/") + .header(ACCEPT, "application/json") + .header(CONTENT_TYPE, "application/json") + .body(hyper::Body::from(r#"{"query":"query { me { name }}"}"#)) + .unwrap(), + ), + ) + .await; + + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + } + .with_subscriber(assert_snapshot_subscriber!( + tracing_core::LevelFilter::ERROR + )) + .await + } } diff --git a/apollo-router/src/axum_factory/mod.rs b/apollo-router/src/axum_factory/mod.rs index e8f0d97cb0..929da384d7 100644 --- a/apollo-router/src/axum_factory/mod.rs +++ b/apollo-router/src/axum_factory/mod.rs @@ -12,6 +12,7 @@ use std::sync::OnceLock; use axum::Router; pub(crate) use axum_http_server_factory::span_mode; pub(crate) use axum_http_server_factory::AxumHttpServerFactory; +pub(crate) use axum_http_server_factory::CanceledRequest; pub(crate) use listeners::ListenAddrAndRouter; static ENDPOINT_CALLBACK: OnceLock Router + Send + Sync>> = OnceLock::new(); diff --git a/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__axum_http_server_factory__tests__request_cancel@logs.snap b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__axum_http_server_factory__tests__request_cancel@logs.snap new file mode 100644 index 0000000000..a7fdad61b8 --- /dev/null +++ b/apollo-router/src/axum_factory/snapshots/apollo_router__axum_factory__axum_http_server_factory__tests__request_cancel@logs.snap @@ -0,0 +1,7 @@ +--- +source: apollo-router/src/axum_factory/axum_http_server_factory.rs +expression: yaml +--- +- fields: {} + level: ERROR + message: "broken pipe: the client closed the connection" diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index b4deb672d6..2ac981ae4b 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -616,6 +616,12 @@ pub(crate) struct Supergraph { /// Query planning options pub(crate) query_planning: QueryPlanning, + + /// abort request handling when the client drops the connection. + /// Default: false. + /// When set to true, some parts of the request pipeline like telemetry will not work properly, + /// but request handling will stop immediately when the client connection is closed. + pub(crate) early_cancel: bool, } fn default_defer_support() -> bool { @@ -632,6 +638,7 @@ impl Supergraph { defer_support: Option, query_planning: Option, reuse_query_fragments: Option, + early_cancel: Option, ) -> Self { Self { listen: listen.unwrap_or_else(default_graphql_listen), @@ -640,6 +647,7 @@ impl Supergraph { defer_support: defer_support.unwrap_or_else(default_defer_support), query_planning: query_planning.unwrap_or_default(), reuse_query_fragments, + early_cancel: early_cancel.unwrap_or_default(), } } } @@ -655,6 +663,7 @@ impl Supergraph { defer_support: Option, query_planning: Option, reuse_query_fragments: Option, + early_cancel: Option, ) -> Self { Self { listen: listen.unwrap_or_else(test_listen), @@ -663,6 +672,7 @@ impl Supergraph { defer_support: defer_support.unwrap_or_else(default_defer_support), query_planning: query_planning.unwrap_or_default(), reuse_query_fragments, + early_cancel: early_cancel.unwrap_or_default(), } } } diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 8f2535d4ef..c2b2217409 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -2686,7 +2686,8 @@ expression: "&schema" "warmed_up_queries": null, "experimental_plans_limit": null, "experimental_paths_limit": null - } + }, + "early_cancel": false }, "type": "object", "properties": { @@ -2695,6 +2696,11 @@ expression: "&schema" "default": true, "type": "boolean" }, + "early_cancel": { + "description": "abort request handling when the client drops the connection. Default: false. When set to true, some parts of the request pipeline like telemetry will not work properly, but request handling will stop immediately when the client connection is closed.", + "default": false, + "type": "boolean" + }, "experimental_reuse_query_fragments": { "description": "Enable reuse of query fragments Default: depends on the federation version", "default": null, diff --git a/apollo-router/src/plugins/coprocessor/execution.rs b/apollo-router/src/plugins/coprocessor/execution.rs index 9bcbf4ce82..6f97a1b2b6 100644 --- a/apollo-router/src/plugins/coprocessor/execution.rs +++ b/apollo-router/src/plugins/coprocessor/execution.rs @@ -12,6 +12,7 @@ use tower_service::Service; use super::externalize_header_map; use super::*; +use crate::batching::BatchQuery; use crate::graphql; use crate::layers::async_checkpoint::OneShotAsyncCheckpointLayer; use crate::layers::ServiceBuilderExt; @@ -289,6 +290,18 @@ where execution_response }; + + // Handle cancelled batch queries + // FIXME: This should be way higher up the call chain so that custom plugins / rhai / etc. can + // automatically work with batched queries and cancellations. + let batch_query_opt = res.context.extensions().lock().remove::(); + if let Some(mut batch_query) = batch_query_opt { + // TODO: How do we reliably get the reason for the coprocessor cancellation here? + batch_query + .signal_cancelled("coprocessor cancelled request at execution layer".to_string()) + .await; + } + return Ok(ControlFlow::Break(res)); } diff --git a/apollo-router/src/plugins/coprocessor/mod.rs b/apollo-router/src/plugins/coprocessor/mod.rs index a79ff4da2b..0a3164dc79 100644 --- a/apollo-router/src/plugins/coprocessor/mod.rs +++ b/apollo-router/src/plugins/coprocessor/mod.rs @@ -30,6 +30,7 @@ use tower::Service; use tower::ServiceBuilder; use tower::ServiceExt; +use crate::batching::BatchQuery; use crate::error::Error; use crate::layers::async_checkpoint::OneShotAsyncCheckpointLayer; use crate::layers::ServiceBuilderExt; @@ -685,6 +686,17 @@ where } } + // Handle cancelled batch queries + // FIXME: This should be way higher up the call chain so that custom plugins / rhai / etc. can + // automatically work with batched queries and cancellations. + let batch_query_opt = res.context.extensions().lock().remove::(); + if let Some(mut batch_query) = batch_query_opt { + // TODO: How do we reliably get the reason for the coprocessor cancellation here? + batch_query + .signal_cancelled("coprocessor cancelled request at router layer".to_string()) + .await; + } + return Ok(ControlFlow::Break(res)); } @@ -1014,6 +1026,18 @@ where subgraph_response }; + + // Handle cancelled batch queries + // FIXME: This should be way higher up the call chain so that custom plugins / rhai / etc. can + // automatically work with batched queries and cancellations. + let batch_query_opt = res.context.extensions().lock().remove::(); + if let Some(mut batch_query) = batch_query_opt { + // TODO: How do we reliably get the reason for the coprocessor cancellation here? + batch_query + .signal_cancelled("coprocessor cancelled request at subgraph layer".to_string()) + .await; + } + return Ok(ControlFlow::Break(res)); } diff --git a/apollo-router/src/plugins/coprocessor/supergraph.rs b/apollo-router/src/plugins/coprocessor/supergraph.rs index 2e995540ae..172062726d 100644 --- a/apollo-router/src/plugins/coprocessor/supergraph.rs +++ b/apollo-router/src/plugins/coprocessor/supergraph.rs @@ -279,6 +279,18 @@ where supergraph_response }; + + // Handle cancelled batch queries + // FIXME: This should be way higher up the call chain so that custom plugins / rhai / etc. can + // automatically work with batched queries and cancellations. + let batch_query_opt = res.context.extensions().lock().remove::(); + if let Some(mut batch_query) = batch_query_opt { + // TODO: How do we reliably get the reason for the coprocessor cancellation here? + batch_query + .signal_cancelled("coprocessor cancelled request at supergraph layer".to_string()) + .await; + } + return Ok(ControlFlow::Break(res)); } diff --git a/apollo-router/src/query_planner/execution.rs b/apollo-router/src/query_planner/execution.rs index 539eb2c808..c22b10a223 100644 --- a/apollo-router/src/query_planner/execution.rs +++ b/apollo-router/src/query_planner/execution.rs @@ -13,9 +13,11 @@ use super::subscription::SubscriptionHandle; use super::DeferredNode; use super::PlanNode; use super::QueryPlan; +use crate::axum_factory::CanceledRequest; use crate::error::Error; use crate::graphql::Request; use crate::graphql::Response; +use crate::json_ext::Object; use crate::json_ext::Path; use crate::json_ext::Value; use crate::json_ext::ValueExt; @@ -218,17 +220,31 @@ impl PlanNode { PlanNode::Fetch(fetch_node) => { let fetch_time_offset = parameters.context.created_at.elapsed().as_nanos() as i64; - let (v, e) = fetch_node - .fetch_node(parameters, parent_value, current_dir) - .instrument(tracing::info_span!( - FETCH_SPAN_NAME, - "otel.kind" = "INTERNAL", - "apollo.subgraph.name" = fetch_node.service_name.as_str(), - "apollo_private.sent_time_offset" = fetch_time_offset - )) - .await; - value = v; - errors = e; + + // The client closed the connection, we are still executing the request pipeline, + // but we won't send unused trafic to subgraph + if parameters + .context + .extensions() + .lock() + .get::() + .is_some() + { + value = Value::Object(Object::default()); + errors = Vec::new(); + } else { + let (v, e) = fetch_node + .fetch_node(parameters, parent_value, current_dir) + .instrument(tracing::info_span!( + FETCH_SPAN_NAME, + "otel.kind" = "INTERNAL", + "apollo.subgraph.name" = fetch_node.service_name.as_str(), + "apollo_private.sent_time_offset" = fetch_time_offset + )) + .await; + value = v; + errors = e; + } } PlanNode::Defer { primary: diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index 1708c111b4..05762a53e0 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -34,6 +34,7 @@ use tower_service::Service; use tracing::Instrument; use super::ClientRequestAccepts; +use crate::axum_factory::CanceledRequest; use crate::batching::Batch; use crate::cache::DeduplicatingCache; use crate::configuration::Batching; @@ -267,6 +268,16 @@ impl RouterService { let (mut parts, mut body) = response.into_parts(); process_vary_header(&mut parts.headers); + if context + .extensions() + .lock() + .get::() + .is_some() + { + parts.status = StatusCode::from_u16(499) + .expect("499 is not a standard status code but common enough"); + } + match body.next().await { None => { tracing::error!("router service is not available to process request",); diff --git a/apollo-router/tests/integration/batching.rs b/apollo-router/tests/integration/batching.rs index 97d6f78a5f..beae021d97 100644 --- a/apollo-router/tests/integration/batching.rs +++ b/apollo-router/tests/integration/batching.rs @@ -474,7 +474,6 @@ async fn it_handles_single_request_cancelled_by_rhai() -> Result<(), BoxError> { } #[tokio::test(flavor = "multi_thread")] -#[ignore] async fn it_handles_cancelled_by_coprocessor() -> Result<(), BoxError> { const REQUEST_COUNT: usize = 2; const COPROCESSOR_CONFIG: &str = include_str!("../fixtures/batching/coprocessor.router.yaml"); @@ -549,7 +548,23 @@ async fn it_handles_cancelled_by_coprocessor() -> Result<(), BoxError> { .await?; // TODO: Fill this in once we know how this response should look - assert_yaml_snapshot!(responses, @""); + assert_yaml_snapshot!(responses, @r###" + --- + - errors: + - message: Subgraph A is not allowed + extensions: + code: ERR_NOT_ALLOWED + - data: + entryB: + index: 0 + - errors: + - message: Subgraph A is not allowed + extensions: + code: ERR_NOT_ALLOWED + - data: + entryB: + index: 1 + "###); Ok(()) } diff --git a/apollo-router/tests/integration/mod.rs b/apollo-router/tests/integration/mod.rs index e5a4509728..97937bac53 100644 --- a/apollo-router/tests/integration/mod.rs +++ b/apollo-router/tests/integration/mod.rs @@ -2,8 +2,6 @@ mod batching; #[path = "../common.rs"] pub(crate) mod common; pub(crate) use common::IntegrationTest; -pub(crate) use common::Telemetry; -pub(crate) use common::ValueExt; mod docs; mod file_upload; diff --git a/docs/source/errors.mdx b/docs/source/errors.mdx index f029c8496a..820f9c1ffc 100644 --- a/docs/source/errors.mdx +++ b/docs/source/errors.mdx @@ -46,6 +46,11 @@ A request's HTTP `Accept` header didn't contain any of the router's supported mi Request traffic exceeded configured rate limits. See [client side traffic shaping](./configuration/traffic-shaping/#client-side-traffic-shaping). + + + +The request was canceled because the client closed the connection, possibly due to a client side timeout. +