diff --git a/.changesets/feat_add_container_scaling_config.md b/.changesets/feat_add_container_scaling_config.md deleted file mode 100644 index a61ec2306e..0000000000 --- a/.changesets/feat_add_container_scaling_config.md +++ /dev/null @@ -1,5 +0,0 @@ -### Support configuring container resource HPA targets ([PR #4776](https://github.com/apollographql/router/pull/4776)) - -Allow configuring container resource HPA targets in the helm chart (https://kubernetes.io/blog/2023/05/02/hpa-container-resource-metric/). - -By [@caugustus](https://github.com/caugustus) in https://github.com/apollographql/router/pull/4776 diff --git a/.changesets/feat_geal_jwks_headers.md b/.changesets/feat_geal_jwks_headers.md deleted file mode 100644 index f0557d3749..0000000000 --- a/.changesets/feat_geal_jwks_headers.md +++ /dev/null @@ -1,5 +0,0 @@ -### Add headers to the JWKS download request ([Issue #4651](https://github.com/apollographql/router/issues/4651)) - -This adds the ability to set static headers on HTTP requests to download a JWKS from an identity provider - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4688 \ No newline at end of file diff --git a/.changesets/feat_geal_jwt_source.md b/.changesets/feat_geal_jwt_source.md deleted file mode 100644 index 96f509a215..0000000000 --- a/.changesets/feat_geal_jwt_source.md +++ /dev/null @@ -1,5 +0,0 @@ -### Support loading JWT from other sources ([PR #4711](https://github.com/apollographql/router/pull/4711)) - -The token can be stored in different headers, but it can also be carried by a cookie. This adds cookies as an alternative source of tokens, and allows multiple alternative sources - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4711 \ No newline at end of file diff --git a/.changesets/feat_geal_query_plan_refresh_ttl.md b/.changesets/feat_geal_query_plan_refresh_ttl.md deleted file mode 100644 index 11347a5ca9..0000000000 --- a/.changesets/feat_geal_query_plan_refresh_ttl.md +++ /dev/null @@ -1,7 +0,0 @@ -### Add an option to refresh expiration on Redis GET ([Issue #4473](https://github.com/apollographql/router/issues/4473)) - -This adds the option to refresh the TTL on Redis entries when they are accessed. We want the query plan cache to act like a LRU, so if a TTL is set in its Redis configuration, it should reset every time it is accessed. - -The option is also available for APQ, but it is disabled for entity caching, since that cache directly manages TTL. - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4604 \ No newline at end of file diff --git a/.changesets/feat_geal_subgraph_unix_socket.md b/.changesets/feat_geal_subgraph_unix_socket.md deleted file mode 100644 index 5e80a0e52c..0000000000 --- a/.changesets/feat_geal_subgraph_unix_socket.md +++ /dev/null @@ -1,6 +0,0 @@ -### Unix socket support for subgraphs ([Issue #3504](https://github.com/apollographql/router/issues/3504)) - -The Router now supports Unix sockets for subgraph connections by specifying URLs in the `unix:///path/to/router.sock` format in the schema. The Router will use stream unix sockets, not datagram ones. It supports compression but not TLS. -Due to the lack of standard for unix socket URLs, and lack of support in the common URL types in Rust, a transformation is applied to to the socket path to parse it: it is encoded in hexadecimal and stored in the authority part. This will have no consequence on the way the router works, but subgraph services will see URLs with the hex encoded host. - -By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4757 \ No newline at end of file diff --git a/.changesets/fix_bnjjj_multipart_test.md b/.changesets/fix_bnjjj_multipart_test.md deleted file mode 100644 index 6f8dc8cf5b..0000000000 --- a/.changesets/fix_bnjjj_multipart_test.md +++ /dev/null @@ -1,6 +0,0 @@ -### Fix chunk formatting in multipart protocol ([Issue #4634](https://github.com/apollographql/router/issues/4634)) - -This PR changes the way we're sending chunks in the stream. Instead of finishing the chunk with `\r\n` we don't send this at the end of our current chunk but instead at the beginning of the next one. For the end users nothing changes but it let us to close the stream with the right final boundary by appending `--\r\n` directly to the last chunk. - - -By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/4681 \ No newline at end of file diff --git a/.changesets/fix_bryn_datadog_trace_id.md b/.changesets/fix_bryn_datadog_trace_id.md deleted file mode 100644 index e26cdb3b02..0000000000 --- a/.changesets/fix_bryn_datadog_trace_id.md +++ /dev/null @@ -1,17 +0,0 @@ -### Attach `dd.trace_id` to JSON formatted log messages ([PR #4764](https://github.com/apollographql/router/pull/4764)) - -To enable correlation between DataDog tracing and logs, `dd.trace_id` must appear as a span attribute on the root of each JSON formatted log message. -Once you configure the `dd.trace_id` attribute in router.yaml, it will automatically be extracted from the root span and attached to the logs: - -```yaml title="router.yaml" -telemetry: - instrumentation: - spans: - mode: spec_compliant - router: - attributes: - dd.trace_id: true -``` - - -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/4764 diff --git a/.changesets/fix_bryn_zipkin_service_name.md b/.changesets/fix_bryn_zipkin_service_name.md deleted file mode 100644 index 62d900e24d..0000000000 --- a/.changesets/fix_bryn_zipkin_service_name.md +++ /dev/null @@ -1,17 +0,0 @@ -### Zipkin service name not populated ([Issue #4807](https://github.com/apollographql/router/issues/4807)) - -Zipkin trace exporter now respects service name configuration from yaml or environment variables. - -For instance to set the service name to `my-app`, you can use the following configuration in your `router.yaml` file: -```yaml -telemetry: - exporters: - tracing: - common: - service_name: my-app - zipkin: - enabled: true - endpoint: default -``` - -By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/4816 diff --git a/.changesets/fix_format_response_index_map_with_capacity.md b/.changesets/fix_format_response_index_map_with_capacity.md deleted file mode 100644 index 2ad32c81eb..0000000000 --- a/.changesets/fix_format_response_index_map_with_capacity.md +++ /dev/null @@ -1,5 +0,0 @@ -### use Object::with_capacity with selection set len in format_response ([PR #4775](https://github.com/apollographql/router/pull/4775)) - -This preallocates output object size in response formatting, bringing a small performance improvement - -By [@xuorig](https://github.com/xuorig) in https://github.com/apollographql/router/pull/4775 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b750bd0a0..7504dee37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,91 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.43.0] - 2024-03-21 + +## 🚀 Features + +### Support configurable heartbeat for subscriptions using the WebSocket protocol ([Issue #4621](https://github.com/apollographql/router/issues/4621)) + +To support GraphQL Subscription WebSocket implementations such as [DGS](https://netflix.github.io/dgs/) that drop idle connections by design, the router adds the ability to configure a heartbeat to keep active connections alive. + +An example router configuration: + +```yaml +subscription: + mode: + passthrough: + all: + path: /graphql + heartbeat_interval: enable # Optional +``` + +By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/4802 + +### Unix socket support for subgraphs ([Issue #3504](https://github.com/apollographql/router/issues/3504)) + +> ⚠️ This is an [Enterprise feature](https://www.apollographql.com/blog/platform/evaluating-apollo-router-understanding-free-and-open-vs-commercial-features/) of the Apollo Router. It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). +> +> If your organization doesn't currently have an Enterprise plan, you can test out this functionality by signing up for a free Enterprise trial. + +The router now supports Unix sockets for subgraph connections by specifying URLs in the `unix:///path/to/router.sock` format in the schema, in addition to coming a valid URL option within [the existing `override_subgraph_url` configuration](https://www.apollographql.com/docs/router/configuration/overview/#subgraph-routing-urls). The router uses Unix stream-oriented sockets (not datagram-oriented). It supports compression but not TLS. + +Due to the lack of standardization of Unix socket URLs (and lack of support in the common URL types in Rust) a transformation is applied to to the socket path to parse it: the host is encoded in hexadecimal and stored in the `authority` part. This will have no consequence on the way the router functions, but [subgraph services](https://www.apollographql.com/docs/router/customizations/overview/#the-request-lifecycle) will receive URLs with the hex-encoded host. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4757 + +### Add an option to refresh expiration on Redis GET ([Issue #4473](https://github.com/apollographql/router/issues/4473)) + +This adds the option to refresh the time-to-live (TTL) on Redis entries when they are accessed. We want the query plan cache to act like an LRU cache (least-recently used), so if a TTL is set in its Redis configuration, it should reset every time it is accessed. + +While the option is also available for APQ, it's disabled for entity caching because that cache manages TTL directly. + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/4604 + +### Helm: Support configuring `ContainerResource` on Horizontal Pod Autoscaler (HPA) targets ([PR #4776](https://github.com/apollographql/router/pull/4776)) + +The router supports configuration of the [`ContainerResource` type metric](https://kubernetes.io/blog/2023/05/02/hpa-container-resource-metric/) on Horizontal Pod Autoscaler (HPA) targets in the Helm chart with Kubernetes v1.27 or later. + +By [@caugustus](https://github.com/caugustus) in https://github.com/apollographql/router/pull/4776 + +## 🐛 Fixes + +### Fix chunk formatting in multipart protocol ([Issue #4634](https://github.com/apollographql/router/issues/4634)) + +Previously, when sending a stream of chunks for HTTP multipart, the router finished each chunk by appending it with `\r\n`. +Now, the router doesn't append `\r\n` to the current chunk but instead prepends it to the next chunk. This enables the router to close a stream with the correct final boundary by appending `--\r\n` directly to the last chunk. + +This PR changes the way we're sending chunks in the stream. Instead of finishing the chunk with `\r\n` we don't send this at the end of our current chunk but instead at the beginning of the next one. For the end users nothing changes but it let us to close the stream with the right final boundary by appending `--\r\n` directly to the last chunk. + +By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/4681 + +### Zipkin service name not populated ([Issue #4807](https://github.com/apollographql/router/issues/4807)) + +The Zipkin trace exporter now respects service name configuration from YAML or environment variables. + +For instance to set the service name to `my-app`, you can use the following configuration in your `router.yaml` file: + +```yaml +telemetry: + exporters: + tracing: + common: + service_name: my-app + zipkin: + enabled: true + endpoint: default +``` + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/4816 + +## 🛠 Maintenance + +### Preallocate response formatting output ([PR #4775](https://github.com/apollographql/router/pull/4775)) + +To improve runtime performance, an internal change to the router's `format_response` now preallocates the output object. + +By [@xuorig](https://github.com/xuorig) in https://github.com/apollographql/router/pull/4775 + # [1.42.0] - 2024-03-12 ## 🚀 Features @@ -42,7 +127,6 @@ telemetry: dd.trace_id: true ``` - By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/4764 # [1.41.1] - 2024-03-08 @@ -173,7 +257,7 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p The `subgraph_response_body` [selector](https://www.apollographql.com/docs/router/configuration/telemetry/instrumentation/selectors/) has been deprecated and replaced with selectors for a response body's constituent elements: `subgraph_response_data` and `subgraph_response_errors`. -When configuring `subgraph_response_data` and `subgraph_response_errors`, both use a JSONPath expression to fetch data or errors from a subgraph response. +When configuring `subgraph_response_data` and `subgraph_response_errors`, both use a JSONPath expression to fetch data or errors from a subgraph response. An example configuration: @@ -192,7 +276,7 @@ By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router ### Add a `.remove` method for headers in Rhai -The router supports a new `.remove` method that enables users to remove headers in a Rhai script. +The router supports a new `.remove` method that enables users to remove headers in a Rhai script. For example: @@ -348,9 +432,9 @@ By [@nicholascioli](https://github.com/nicholascioli) in https://github.com/apol ### Add selector to get all baggage key values in span attributes ([Issue #4425](https://github.com/apollographql/router/issues/4425)) -Previously, baggage items were configured as standard attributes in `router.yaml`, and adding a new baggage item required a configuration update and router rerelease. +Previously, baggage items were configured as standard attributes in `router.yaml`, and adding a new baggage item required a configuration update and router rerelease. -This release supports a new configuration that enables baggage items to be added automatically as span attributes. +This release supports a new configuration that enables baggage items to be added automatically as span attributes. If you have several baggage items and would like to add all of them directly as span attributes (for example, `baggage: my_item=test, my_second_item=bar`), setting `baggage: true` will add automatically add two span attributes, `my_item=test` and `my_second_item=bar`. diff --git a/Cargo.lock b/Cargo.lock index c46d2bc0b8..186857ec6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,7 +251,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.42.0" +version = "1.43.0" dependencies = [ "access-json", "anyhow", @@ -410,7 +410,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.42.0" +version = "1.43.0" dependencies = [ "apollo-parser", "apollo-router", @@ -426,7 +426,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.42.0" +version = "1.43.0" dependencies = [ "anyhow", "cargo-scaffold", diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index be8b322344..7aacfbe0ae 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.42.0" +version = "1.43.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 7b543518a8..37bd6ab990 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.42.0" +version = "1.43.0" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.toml b/apollo-router-scaffold/templates/base/Cargo.toml index b11ff76ab5..7080d3df9a 100644 --- a/apollo-router-scaffold/templates/base/Cargo.toml +++ b/apollo-router-scaffold/templates/base/Cargo.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.42.0" +apollo-router = "1.43.0" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.toml index 0271cfc8a4..f64a7f963c 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.42.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.43.0" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index dcabb4e022..d656c31166 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.42.0" +version = "1.43.0" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" 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 2aab96b824..7012246d6f 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 @@ -2404,15 +2404,24 @@ expression: "&schema" "properties": { "heartbeat_interval": { "description": "Heartbeat interval for callback mode (default: 5secs)", - "default": "5s", + "default": "enabled", "anyOf": [ { + "description": "disable heartbeat", "type": "string", "enum": [ "disabled" ] }, { + "description": "enable with default interval of 5s", + "type": "string", + "enum": [ + "enabled" + ] + }, + { + "description": "enable with custom interval, e.g. '100ms', '10s' or '1m'", "type": "string" } ] @@ -2464,6 +2473,30 @@ expression: "&schema" "default": null, "type": "object", "properties": { + "heartbeat_interval": { + "description": "Heartbeat interval for graphql-ws protocol (default: disabled)", + "default": "disabled", + "anyOf": [ + { + "description": "disable heartbeat", + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "description": "enable with default interval of 5s", + "type": "string", + "enum": [ + "enabled" + ] + }, + { + "description": "enable with custom interval, e.g. '100ms', '10s' or '1m'", + "type": "string" + } + ] + }, "path": { "description": "Path on which WebSockets are listening", "default": null, @@ -2491,6 +2524,30 @@ expression: "&schema" "description": "WebSocket configuration for a specific subgraph", "type": "object", "properties": { + "heartbeat_interval": { + "description": "Heartbeat interval for graphql-ws protocol (default: disabled)", + "default": "disabled", + "anyOf": [ + { + "description": "disable heartbeat", + "type": "string", + "enum": [ + "disabled" + ] + }, + { + "description": "enable with default interval of 5s", + "type": "string", + "enum": [ + "enabled" + ] + }, + { + "description": "enable with custom interval, e.g. '100ms', '10s' or '1m'", + "type": "string" + } + ] + }, "path": { "description": "Path on which WebSockets are listening", "default": null, diff --git a/apollo-router/src/plugins/subscription.rs b/apollo-router/src/plugins/subscription.rs index 335e3e770b..1662e16cc9 100644 --- a/apollo-router/src/plugins/subscription.rs +++ b/apollo-router/src/plugins/subscription.rs @@ -112,7 +112,7 @@ impl SubscriptionModeConfig { if callback_cfg.subgraphs.contains(service_name) || callback_cfg.subgraphs.is_empty() { let callback_cfg = CallbackMode { public_url: callback_cfg.public_url.clone(), - heartbeat_interval: callback_cfg.heartbeat_interval.clone(), + heartbeat_interval: callback_cfg.heartbeat_interval, listen: callback_cfg.listen.clone(), path: callback_cfg.path.clone(), subgraphs: HashSet::new(), // We don't need it @@ -151,7 +151,7 @@ pub(crate) struct CallbackMode { pub(crate) public_url: url::Url, /// Heartbeat interval for callback mode (default: 5secs) - #[serde(default = "HeartbeatInterval::default")] + #[serde(default = "HeartbeatInterval::new_enabled")] pub(crate) heartbeat_interval: HeartbeatInterval, // `skip_serializing` We don't need it in the context /// Listen address on which the callback must listen (default: 127.0.0.1:4000) @@ -168,25 +168,45 @@ pub(crate) struct CallbackMode { pub(crate) subgraphs: HashSet, } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case", untagged)] pub(crate) enum HeartbeatInterval { + /// disable heartbeat Disabled(Disabled), + /// enable with default interval of 5s + Enabled(Enabled), + /// enable with custom interval, e.g. '100ms', '10s' or '1m' #[serde(with = "humantime_serde")] #[schemars(with = "String")] Duration(Duration), } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +impl HeartbeatInterval { + pub(crate) fn new_enabled() -> Self { + Self::Enabled(Enabled::Enabled) + } + pub(crate) fn new_disabled() -> Self { + Self::Disabled(Disabled::Disabled) + } + pub(crate) fn into_option(self) -> Option { + match self { + Self::Disabled(_) => None, + Self::Enabled(_) => Some(Duration::from_secs(5)), + Self::Duration(duration) => Some(duration), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub(crate) enum Disabled { Disabled, } -impl Default for HeartbeatInterval { - fn default() -> Self { - Self::Duration(Duration::from_secs(5)) - } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum Enabled { + Enabled, } /// Using websocket to directly connect to subgraph @@ -197,14 +217,19 @@ pub(crate) struct PassthroughMode { subgraph: SubgraphPassthroughMode, } -#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields, default)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] /// WebSocket configuration for a specific subgraph pub(crate) struct WebSocketConfiguration { /// Path on which WebSockets are listening + #[serde(default)] pub(crate) path: Option, /// Which WebSocket GraphQL protocol to use for this subgraph possible values are: 'graphql_ws' | 'graphql_transport_ws' (default: graphql_ws) + #[serde(default)] pub(crate) protocol: WebSocketProtocol, + /// Heartbeat interval for graphql-ws protocol (default: disabled) + #[serde(default = "HeartbeatInterval::new_disabled")] + pub(crate) heartbeat_interval: HeartbeatInterval, } fn default_path() -> String { @@ -228,21 +253,17 @@ impl Plugin for Subscription { .clone(), ); #[cfg(not(test))] - match init - .config - .mode - .callback - .as_ref() - .expect("we checked in the condition the callback conf") - .heartbeat_interval - { - HeartbeatInterval::Duration(duration) => { - init.notify.set_ttl(Some(duration)).await?; - } - HeartbeatInterval::Disabled(_) => { - init.notify.set_ttl(None).await?; - } - } + init.notify + .set_ttl( + init.config + .mode + .callback + .as_ref() + .expect("we checked in the condition the callback conf") + .heartbeat_interval + .into_option(), + ) + .await?; } Ok(Subscription { diff --git a/apollo-router/src/protocols/websocket.rs b/apollo-router/src/protocols/websocket.rs index 4dc8663b9f..4e68e3bd6d 100644 --- a/apollo-router/src/protocols/websocket.rs +++ b/apollo-router/src/protocols/websocket.rs @@ -3,6 +3,7 @@ use std::task::Poll; use std::time::Duration; use futures::future; +use futures::stream::SplitStream; use futures::Future; use futures::Sink; use futures::SinkExt; @@ -16,6 +17,7 @@ use serde::Serialize; use serde_json_bytes::Value; use tokio::io::AsyncRead; use tokio::io::AsyncWrite; +use tokio_stream::wrappers::IntervalStream; use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::Message; @@ -213,21 +215,19 @@ impl ServerMessage { } } -pin_project! { pub(crate) struct GraphqlWebSocket { - #[pin] stream: S, id: String, protocol: WebSocketProtocol, - // Booleans for state machine when closing the stream - completed: bool, - terminated: bool, -} } impl GraphqlWebSocket where - S: Stream> + Sink + std::marker::Unpin, + S: Stream> + + Sink + + std::marker::Unpin + + std::marker::Send + + 'static, { pub(crate) async fn new( mut stream: S, @@ -289,10 +289,37 @@ where stream, id, protocol, - completed: false, - terminated: false, }) } + + pub(crate) async fn into_subscription( + mut self, + request: graphql::Request, + heartbeat_interval: Option, + ) -> Result, graphql::Error> { + tracing::info!( + monotonic_counter + .apollo + .router + .operations + .subscriptions + .events = 1u64, + subscriptions.mode = "passthrough" + ); + + self.stream + .send(self.protocol.subscribe(self.id.to_string(), request)) + .await + .map(|_| { + SubscriptionStream::new(self.stream, self.id, self.protocol, heartbeat_interval) + }) + .map_err(|_err| { + graphql::Error::builder() + .message("cannot send to websocket connection") + .extension_code("WEBSOCKET_CONNECTION_ERROR") + .build() + }) + } } #[derive(thiserror::Error, Debug)] @@ -361,7 +388,129 @@ where }) } -impl Stream for GraphqlWebSocket +pub(crate) struct SubscriptionStream { + inner_stream: SplitStream>, + close_signal: Option>, +} + +impl SubscriptionStream +where + S: Stream> + + Sink + + std::marker::Unpin + + std::marker::Send + + 'static, +{ + pub(crate) fn new( + stream: S, + id: String, + protocol: WebSocketProtocol, + heartbeat_interval: Option, + ) -> Self { + let (mut sink, inner_stream) = InnerStream::new(stream, id, protocol).split(); + let (close_signal, close_sentinel) = tokio::sync::oneshot::channel::<()>(); + + tokio::task::spawn(async move { + if let (WebSocketProtocol::GraphqlWs, Some(duration)) = (protocol, heartbeat_interval) { + let mut interval = + tokio::time::interval_at(tokio::time::Instant::now() + duration, duration); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut heartbeat_stream = IntervalStream::new(interval) + .map(|_| { + Ok(ClientMessage::Ping { + payload: Some(serde_json_bytes::Value::String( + "APOLLO_ROUTER_HEARTBEAT".into(), + )), + }) + }) + .take_until(close_sentinel); + if let Err(err) = sink.send_all(&mut heartbeat_stream).await { + tracing::trace!("cannot send heartbeat: {err:?}"); + if let Some(close_sentinel) = heartbeat_stream.take_future() { + if let Err(err) = close_sentinel.await { + tracing::trace!("cannot shutdown sink: {err:?}"); + } + } + } + } else if let Err(err) = close_sentinel.await { + tracing::trace!("cannot shutdown sink: {err:?}"); + }; + + tracing::info!( + monotonic_counter + .apollo + .router + .operations + .subscriptions + .events = 1u64, + subscriptions.mode = "passthrough", + subscriptions.complete = true + ); + + if let Err(err) = sink.close().await { + tracing::trace!("cannot close the websocket stream: {err:?}"); + } + }); + + Self { + inner_stream, + close_signal: Some(close_signal), + } + } +} + +impl Drop for SubscriptionStream { + fn drop(&mut self) { + if let Some(close_signal) = self.close_signal.take() { + if let Err(err) = close_signal.send(()) { + tracing::trace!("cannot close the websocket stream: {err:?}"); + } + } + } +} + +impl Stream for SubscriptionStream +where + S: Stream> + Sink + std::marker::Unpin, +{ + type Item = graphql::Response; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.inner_stream.poll_next_unpin(cx) + } +} + +pin_project! { +struct InnerStream { + #[pin] + stream: S, + id: String, + protocol: WebSocketProtocol, + // Booleans for state machine when closing the stream + completed: bool, + terminated: bool, +} +} + +impl InnerStream +where + S: Stream> + Sink + std::marker::Unpin, +{ + fn new(stream: S, id: String, protocol: WebSocketProtocol) -> Self { + Self { + stream, + id, + protocol, + completed: false, + terminated: false, + } + } +} + +impl Stream for InnerStream where S: Stream> + Sink, { @@ -419,7 +568,7 @@ where } } -impl Sink for GraphqlWebSocket +impl Sink for InnerStream where S: Stream> + Sink, { @@ -444,26 +593,15 @@ where }) } - fn start_send(self: Pin<&mut Self>, item: graphql::Request) -> Result<(), Self::Error> { + fn start_send(self: Pin<&mut Self>, item: ClientMessage) -> Result<(), Self::Error> { let mut this = self.project(); - tracing::info!( - monotonic_counter - .apollo - .router - .operations - .subscriptions - .events = 1u64, - subscriptions.mode = "passthrough" - ); - Pin::new(&mut this.stream) - .start_send(this.protocol.subscribe(this.id.to_string(), item)) - .map_err(|_err| { - graphql::Error::builder() - .message("cannot send to websocket connection") - .extension_code("WEBSOCKET_CONNECTION_ERROR") - .build() - }) + Pin::new(&mut this.stream).start_send(item).map_err(|_err| { + graphql::Error::builder() + .message("cannot send to websocket connection") + .extension_code("WEBSOCKET_CONNECTION_ERROR") + .build() + }) } fn poll_flush( @@ -483,16 +621,6 @@ where self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - tracing::info!( - monotonic_counter - .apollo - .router - .operations - .subscriptions - .events = 1u64, - subscriptions.mode = "passthrough", - subscriptions.complete = true - ); let mut this = self.project(); if !*this.completed { match Pin::new( @@ -548,7 +676,7 @@ mod tests { use axum::routing::get; use axum::Router; use axum::Server; - use futures::StreamExt; + use futures::FutureExt; use http::HeaderValue; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::client::IntoClientRequest; @@ -559,6 +687,7 @@ mod tests { async fn emulate_correct_websocket_server_new_protocol( send_ping: bool, + heartbeat_interval: Option, port: Option, ) -> SocketAddr { let ws_handler = move |ws: WebSocketUpgrade| async move { @@ -616,6 +745,26 @@ mod tests { .await .unwrap(); + if let Some(duration) = heartbeat_interval { + tokio::time::pause(); + assert!( + socket.next().now_or_never().is_none(), + "It should be no pending messages" + ); + + tokio::time::sleep(duration).await; + let ping_message = socket.next().await.unwrap().unwrap(); + assert_eq!(ping_message, AxumWsMessage::Text( + serde_json::to_string(&ClientMessage::Ping { payload: Some(serde_json_bytes::Value::String("APOLLO_ROUTER_HEARTBEAT".into())) }).unwrap(), + )); + + assert!( + socket.next().now_or_never().is_none(), + "It should be no pending messages" + ); + tokio::time::resume(); + } + socket .send(AxumWsMessage::Text( serde_json::to_string(&ServerMessage::Next { id: client_id.clone().unwrap(), payload: graphql::Response::builder().data(serde_json_bytes::json!({"userWasCreated": {"username": "ada_lovelace"}})).build() }).unwrap(), @@ -774,16 +923,27 @@ mod tests { #[tokio::test] async fn test_ws_connection_new_proto_with_ping() { - test_ws_connection_new_proto(true, None).await + test_ws_connection_new_proto(true, None, None).await } #[tokio::test] async fn test_ws_connection_new_proto_without_ping() { - test_ws_connection_new_proto(false, None).await + test_ws_connection_new_proto(false, None, None).await + } + + #[tokio::test] + async fn test_ws_connection_new_proto_with_heartbeat() { + test_ws_connection_new_proto(false, Some(tokio::time::Duration::from_secs(60)), None).await } - async fn test_ws_connection_new_proto(send_ping: bool, port: Option) { - let socket_addr = emulate_correct_websocket_server_new_protocol(send_ping, port).await; + async fn test_ws_connection_new_proto( + send_ping: bool, + heartbeat_interval: Option, + port: Option, + ) { + let socket_addr = + emulate_correct_websocket_server_new_protocol(send_ping, heartbeat_interval, port) + .await; let url = url::Url::parse(format!("ws://{}/ws", socket_addr).as_str()).unwrap(); let mut request = url.into_client_request().unwrap(); request.headers_mut().insert( @@ -793,7 +953,7 @@ mod tests { let (ws_stream, _resp) = connect_async(request).await.unwrap(); let sub_uuid = Uuid::new_v4(); - let gql_stream = GraphqlWebSocket::new( + let gql_socket = GraphqlWebSocket::new( convert_websocket_stream(ws_stream, sub_uuid.to_string()), sub_uuid.to_string(), WebSocketProtocol::GraphqlWs, @@ -805,13 +965,13 @@ mod tests { .unwrap(); let sub = "subscription {\n userWasCreated {\n username\n }\n}"; - let (mut gql_sink, mut gql_read_stream) = gql_stream.split(); - let _handle = tokio::task::spawn(async move { - gql_sink - .send(graphql::Request::builder().query(sub).build()) - .await - .unwrap(); - }); + let mut gql_read_stream = gql_socket + .into_subscription( + graphql::Request::builder().query(sub).build(), + heartbeat_interval, + ) + .await + .unwrap(); let next_payload = gql_read_stream.next().await.unwrap(); assert_eq!(next_payload, graphql::Response::builder() @@ -834,7 +994,7 @@ mod tests { .build() ); assert!( - gql_read_stream.next().await.is_none(), + gql_read_stream.next().now_or_never().is_none(), "It should be completed" ); } @@ -860,7 +1020,7 @@ mod tests { let (ws_stream, _resp) = connect_async(request).await.unwrap(); let sub_uuid = Uuid::new_v4(); - let gql_stream = GraphqlWebSocket::new( + let gql_socket = GraphqlWebSocket::new( convert_websocket_stream(ws_stream, sub_uuid.to_string()), sub_uuid.to_string(), WebSocketProtocol::SubscriptionsTransportWs, @@ -870,14 +1030,10 @@ mod tests { .unwrap(); let sub = "subscription {\n userWasCreated {\n username\n }\n}"; - let (mut gql_sink, mut gql_read_stream) = gql_stream.split(); - let _handle = tokio::task::spawn(async move { - gql_sink - .send(graphql::Request::builder().query(sub).build()) - .await - .unwrap(); - gql_sink.close().await.unwrap(); - }); + let mut gql_read_stream = gql_socket + .into_subscription(graphql::Request::builder().query(sub).build(), None) + .await + .unwrap(); let next_payload = gql_read_stream.next().await.unwrap(); assert_eq!(next_payload, graphql::Response::builder() @@ -900,7 +1056,7 @@ mod tests { .build() ); assert!( - gql_read_stream.next().await.is_none(), + gql_read_stream.next().now_or_never().is_none(), "It should be completed" ); } diff --git a/apollo-router/src/services/subgraph_service.rs b/apollo-router/src/services/subgraph_service.rs index 394eaf4135..048541c32e 100644 --- a/apollo-router/src/services/subgraph_service.rs +++ b/apollo-router/src/services/subgraph_service.rs @@ -8,7 +8,6 @@ use std::task::Poll; use bytes::Bytes; use futures::future::BoxFuture; -use futures::SinkExt; use futures::StreamExt; use futures::TryFutureExt; use http::header::ACCEPT; @@ -47,7 +46,6 @@ use crate::plugins::authentication::subgraph::SigningParamsConfig; use crate::plugins::file_uploads; use crate::plugins::subscription::create_verifier; use crate::plugins::subscription::CallbackMode; -use crate::plugins::subscription::HeartbeatInterval; use crate::plugins::subscription::SubscriptionConfig; use crate::plugins::subscription::SubscriptionMode; use crate::plugins::subscription::WebSocketConfiguration; @@ -330,12 +328,10 @@ impl tower::Service for SubgraphService { subscription_id, callback_url, verifier, - heartbeat_interval_ms: match heartbeat_interval { - HeartbeatInterval::Disabled(_) => 0, - HeartbeatInterval::Duration(duration) => { - duration.as_millis() as u64 - } - }, + heartbeat_interval_ms: heartbeat_interval + .into_option() + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0), }; body.extensions.insert( "subscription", @@ -574,7 +570,7 @@ async fn call_websocket( ); } - let mut gql_stream = GraphqlWebSocket::new( + let gql_socket = GraphqlWebSocket::new( convert_websocket_stream(ws_stream, subscription_hash.clone()), subscription_hash, subgraph_cfg.protocol, @@ -586,14 +582,14 @@ async fn call_websocket( reason: "cannot get the GraphQL websocket stream".to_string(), })?; - gql_stream - .send(body) + let gql_stream = gql_socket + .into_subscription(body, subgraph_cfg.heartbeat_interval.into_option()) .await .map_err(|err| FetchError::SubrequestWsError { service: service_name, reason: format!("cannot send the subgraph request to websocket stream: {err:?}"), })?; - let (mut gql_sink, gql_stream) = gql_stream.split(); + let (handle_sink, handle_stream) = handle.split(); tokio::task::spawn(async move { @@ -601,10 +597,6 @@ async fn call_websocket( .map(Ok::<_, graphql::Error>) .forward(handle_sink) .await; - - if let Err(err) = gql_sink.close().await { - tracing::trace!("cannot close the websocket stream: {err:?}"); - } }); subscription_stream_tx.send(Box::pin(handle_stream)).await?; @@ -1054,7 +1046,7 @@ mod tests { use crate::graphql::Error; use crate::graphql::Request; use crate::graphql::Response; - use crate::plugins::subscription::Disabled; + use crate::plugins::subscription::HeartbeatInterval; use crate::plugins::subscription::SubgraphPassthroughMode; use crate::plugins::subscription::SubscriptionModeConfig; use crate::plugins::subscription::SUBSCRIPTION_CALLBACK_HMAC_KEY; @@ -1639,7 +1631,7 @@ mod tests { listen: None, path: Some("/testcallback".to_string()), subgraphs: vec![String::from("testbis")].into_iter().collect(), - heartbeat_interval: HeartbeatInterval::Disabled(Disabled::Disabled), + heartbeat_interval: HeartbeatInterval::new_disabled(), }), passthrough: Some(SubgraphPassthroughMode { all: None, @@ -1648,6 +1640,7 @@ mod tests { WebSocketConfiguration { path: Some(String::from("/ws")), protocol: WebSocketProtocol::default(), + heartbeat_interval: HeartbeatInterval::new_disabled(), }, )] .into(), diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 217435b14f..abca765f21 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.42.0 + image: ghcr.io/apollographql/router:v1.43.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 9fb80a56e9..c2a84e8c86 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.42.0 + image: ghcr.io/apollographql/router:v1.43.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index 1c2822fa2d..05853415fa 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.42.0 + image: ghcr.io/apollographql/router:v1.43.0 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx b/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx index 75da3b2f2e..4df843bea2 100644 --- a/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx +++ b/docs/source/configuration/telemetry/instrumentation/standard-instruments.mdx @@ -104,3 +104,9 @@ The initial call to Uplink during router startup is not reflected in metrics. - `apollo.router.telemetry.studio.reports` - The number of reports submitted to GraphOS Studio by the Router. - `report.type`: The type of report submitted: "traces" or "metrics" + +### Deprecated + +The following metrics have been deprecated and should not be used. + +- `apollo_router_span` - **Deprecated**—use `apollo_router_processing_time` instead. diff --git a/docs/source/executing-operations/subscription-support.mdx b/docs/source/executing-operations/subscription-support.mdx index 48c2fae617..2f2fe92d9c 100644 --- a/docs/source/executing-operations/subscription-support.mdx +++ b/docs/source/executing-operations/subscription-support.mdx @@ -101,9 +101,11 @@ subscription: reviews: # Overrides settings for the 'reviews' subgraph path: /ws # Absolute path that overrides '/subscriptions' defined above protocol: graphql_ws # The WebSocket-based subprotocol to use for subscription communication (Default: graphql_ws) + heartbeat_interval: 10s # Optional and 'disable' by default, also supports 'enable' (set 5s interval) and custom values for intervals, e.g. '100ms', '10s', '1m'. ``` This example enables subscriptions in **passthrough mode**, which uses long-lived WebSocket connections. +Note: If your subgraph implementation (e.g. [DGS](https://netflix.github.io/dgs/)) can close idle connection, set `heartbest_interval` to keep connection alive. The router supports the following WebSocket subprotocols, specified via the `protocol` option: @@ -160,7 +162,7 @@ subscription: - accounts ``` -You can disable the heartbeat by setting `heartbeat_interval_ms: disabled`. This is useful for example if you're running in callback mode in an infrastructure based on lambda functions, where you prefer neither to send heartbeats nor to keep a lambda awake just to send heartbeats to subscriptions. +You can disable the heartbeat by setting `heartbeat_interval: disabled`. This is useful for example if you're running in callback mode in an infrastructure based on lambda functions, where you prefer neither to send heartbeats nor to keep a lambda awake just to send heartbeats to subscriptions. diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 04ac1b1deb..810f6fa197 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.42.0 +version: 1.43.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.42.0" +appVersion: "v1.43.0" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index b355b50d38..145a988855 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.42.0](https://img.shields.io/badge/Version-1.42.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.42.0](https://img.shields.io/badge/AppVersion-v1.42.0-informational?style=flat-square) +![Version: 1.43.0](https://img.shields.io/badge/Version-1.43.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.43.0](https://img.shields.io/badge/AppVersion-v1.43.0-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.42.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.43.0 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.42.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.42.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.43.0 --values my-values.yaml ``` _See [configuration](#configuration) below._ diff --git a/licenses.html b/licenses.html index e9da5dc90c..1012db7763 100644 --- a/licenses.html +++ b/licenses.html @@ -45,7 +45,7 @@

Third Party Licenses

Overview of licenses:

  • Apache License 2.0 (514)
  • -
  • MIT License (153)
  • +
  • MIT License (155)
  • BSD 3-Clause "New" or "Revised" License (12)
  • ISC License (11)
  • BSD 2-Clause "Simplified" License (3)
  • @@ -13859,6 +13859,33 @@

    Used by:

    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2015-2020 Doug Tangren
    +
    +Permission is hereby granted, free of charge, to any person obtaining
    +a copy of this software and associated documentation files (the
    +"Software"), to deal in the Software without restriction, including
    +without limitation the rights to use, copy, modify, merge, publish,
    +distribute, sublicense, and/or sell copies of the Software, and to
    +permit persons to whom the Software is furnished to do so, subject to
    +the following conditions:
    +
    +The above copyright notice and this permission notice shall be
    +included in all copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  • MIT License

    @@ -14949,6 +14976,7 @@

    Used by:

    MIT License

    Used by:

      +
    • strum
    • strum
    • strum_macros
    • strum_macros
    • diff --git a/scripts/install.sh b/scripts/install.sh index 38af80eee4..764d578d20 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa # Router version defined in apollo-router's Cargo.toml # Note: Change this line manually during the release steps. -PACKAGE_VERSION="v1.42.0" +PACKAGE_VERSION="v1.43.0" download_binary() { downloader --check