Skip to content

Commit 2fe644d

Browse files
committed
Make sure Duration in config is treated as String
1 parent 09e1181 commit 2fe644d

File tree

5 files changed

+111
-135
lines changed

5 files changed

+111
-135
lines changed

.gemini/styleguide.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ async fn handle(user: &User, req: &Request) -> Result<Response> {
8181
Ok(...)
8282
}
8383

84+
---
85+
86+
## `std::time::Duration` in `router-config` Crate
87+
88+
When using `std::time::Duration` in the `router-config` crate **only**, you **must** add both serde and schemars attributes:
89+
90+
```rust
91+
use std::time::Duration;
92+
93+
#[derive(serde::Serialize, serde::Deserialize)]
94+
struct Config {
95+
#[serde(
96+
deserialize_with = "humantime_serde::deserialize",
97+
serialize_with = "humantime_serde::serialize",
98+
)]
99+
#[schemars(with = "String")]
100+
timeout: Duration,
101+
}
102+
```
103+
104+
- **`#[serde(...)]`** enables human-readable time formats (e.g., `"30s"`, `"1m30s"`) in config files.
105+
- **`#[schemars(with = "String")]`** ensures the JSON schema correctly represents the field as a string, not as a numeric value.
106+
107+
**Important:** This pattern applies **only** to the `router-config` crate.
108+
109+
---
110+
84111
## Releasing
85112

86113
We are using `knope` with changesets for declaring changes. If you detect a new file in a PR under `.changeset/` directory, please confirm the following rules:

docs/README.md

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,7 +1746,7 @@ The path can be either absolute or relative to the router's working directory.
17461746
|Name|Type|Description|Required|
17471747
|----|----|-----------|--------|
17481748
|**path**|`string`|The path to the supergraph file.<br/><br/>Can also be set using the `SUPERGRAPH_FILE_PATH` environment variable.<br/>Format: `"path"`<br/>|yes|
1749-
|[**poll\_interval**](#option1poll_interval)|`object`, `null`|Optional interval at which the file should be polled for changes.<br/>|yes|
1749+
|**poll\_interval**|`string`|Optional interval at which the file should be polled for changes.<br/>If not provided, the file will only be loaded once when the router starts.<br/>|no|
17501750
|**source**|`string`|Constant Value: `"file"`<br/>|yes|
17511751

17521752
**Additional Properties:** not allowed
@@ -1768,11 +1768,11 @@ Loads a supergraph from Hive Console CDN.
17681768
|Name|Type|Description|Required|
17691769
|----|----|-----------|--------|
17701770
|**accept\_invalid\_certs**|`boolean`|Whether to accept invalid TLS certificates when connecting to the Hive Console CDN.<br/>Default: `false`<br/>|no|
1771-
|[**connect\_timeout**](#option2connect_timeout)|`object`|Connect timeout for the Hive Console CDN requests.<br/>Default: `"10s"`<br/>|yes|
1771+
|**connect\_timeout**|`string`|Connect timeout for the Hive Console CDN requests.<br/>Default: `"10s"`<br/>|no|
17721772
|**endpoint**|`string`|The CDN endpoint from Hive Console target.<br/><br/>Can also be set using the `HIVE_CDN_ENDPOINT` environment variable.<br/>|yes|
17731773
|**key**|`string`|The CDN Access Token with from the Hive Console target.<br/><br/>Can also be set using the `HIVE_CDN_KEY` environment variable.<br/>|yes|
1774-
|[**poll\_interval**](#option2poll_interval)|`object`|Interval at which the Hive Console should be polled for changes.<br/>Default: `"10s"`<br/>|yes|
1775-
|[**request\_timeout**](#option2request_timeout)|`object`|Request timeout for the Hive Console CDN requests.<br/>Default: `"1m"`<br/>|yes|
1774+
|**poll\_interval**|`string`|Interval at which the Hive Console should be polled for changes.<br/><br/>Can also be set using the `HIVE_CDN_POLL_INTERVAL` environment variable.<br/>Default: `"10s"`<br/>|no|
1775+
|**request\_timeout**|`string`|Request timeout for the Hive Console CDN requests.<br/>Default: `"1m"`<br/>|no|
17761776
|[**retry\_policy**](#option2retry_policy)|`object`|Interval at which the Hive Console should be polled for changes.<br/>Default: `{"max_retries":10}`<br/>|yes|
17771777
|**source**|`string`|Constant Value: `"hive"`<br/>|yes|
17781778

@@ -1790,89 +1790,6 @@ retry_policy:
17901790
```
17911791

17921792

1793-
<a name="option1poll_interval"></a>
1794-
## Option 1: poll\_interval: object,null
1795-
1796-
Optional interval at which the file should be polled for changes.
1797-
If not provided, the file will only be loaded once when the router starts.
1798-
1799-
1800-
**Properties**
1801-
1802-
|Name|Type|Description|Required|
1803-
|----|----|-----------|--------|
1804-
|**nanos**|`integer`|Format: `"uint32"`<br/>Minimum: `0`<br/>|yes|
1805-
|**secs**|`integer`|Format: `"uint64"`<br/>Minimum: `0`<br/>|yes|
1806-
1807-
**Example**
1808-
1809-
```yaml
1810-
{}
1811-
1812-
```
1813-
1814-
<a name="option2connect_timeout"></a>
1815-
## Option 2: connect\_timeout: object
1816-
1817-
Connect timeout for the Hive Console CDN requests.
1818-
1819-
1820-
**Properties**
1821-
1822-
|Name|Type|Description|Required|
1823-
|----|----|-----------|--------|
1824-
|**nanos**|`integer`|Format: `"uint32"`<br/>Minimum: `0`<br/>|yes|
1825-
|**secs**|`integer`|Format: `"uint64"`<br/>Minimum: `0`<br/>|yes|
1826-
1827-
**Example**
1828-
1829-
```yaml
1830-
10s
1831-
1832-
```
1833-
1834-
<a name="option2poll_interval"></a>
1835-
## Option 2: poll\_interval: object
1836-
1837-
Interval at which the Hive Console should be polled for changes.
1838-
1839-
Can also be set using the `HIVE_CDN_POLL_INTERVAL` environment variable.
1840-
1841-
1842-
**Properties**
1843-
1844-
|Name|Type|Description|Required|
1845-
|----|----|-----------|--------|
1846-
|**nanos**|`integer`|Format: `"uint32"`<br/>Minimum: `0`<br/>|yes|
1847-
|**secs**|`integer`|Format: `"uint64"`<br/>Minimum: `0`<br/>|yes|
1848-
1849-
**Example**
1850-
1851-
```yaml
1852-
10s
1853-
1854-
```
1855-
1856-
<a name="option2request_timeout"></a>
1857-
## Option 2: request\_timeout: object
1858-
1859-
Request timeout for the Hive Console CDN requests.
1860-
1861-
1862-
**Properties**
1863-
1864-
|Name|Type|Description|Required|
1865-
|----|----|-----------|--------|
1866-
|**nanos**|`integer`|Format: `"uint32"`<br/>Minimum: `0`<br/>|yes|
1867-
|**secs**|`integer`|Format: `"uint64"`<br/>Minimum: `0`<br/>|yes|
1868-
1869-
**Example**
1870-
1871-
```yaml
1872-
1m
1873-
1874-
```
1875-
18761793
<a name="option2retry_policy"></a>
18771794
## Option 2: retry\_policy: object
18781795

@@ -1964,7 +1881,7 @@ Optional per-subgraph configurations that will override the default configuratio
19641881
|Name|Type|Description|Required|
19651882
|----|----|-----------|--------|
19661883
|**dedupe\_enabled**|`boolean`, `null`|Enables/disables request deduplication to subgraphs.<br/><br/>When requests exactly matches the hashing mechanism (e.g., subgraph name, URL, headers, query, variables), and are executed at the same time, they will<br/>be deduplicated by sharing the response of other in-flight requests.<br/>|no|
1967-
|**pool\_idle\_timeout\_seconds**|`string`|Timeout for idle sockets being kept-alive.<br/>|yes|
1884+
|**pool\_idle\_timeout**|`string`|Timeout for idle sockets being kept-alive.<br/>|yes|
19681885
|**request\_timeout**||Optional timeout configuration for requests to subgraphs.<br/><br/>Example with a fixed duration:<br/>```yaml<br/> timeout:<br/> duration: 5s<br/>```<br/><br/>Or with a VRL expression that can return a duration based on the operation kind:<br/>```yaml<br/> timeout:<br/> expression: \|<br/> if (.request.operation.type == "mutation") {<br/> "10s"<br/> } else {<br/> "15s"<br/> }<br/>```<br/>|no|
19691886

19701887
**Additional Properties:** not allowed

lib/executor/src/executors/map.rs

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use std::{
66

77
use bytes::{BufMut, Bytes, BytesMut};
88
use dashmap::DashMap;
9-
use hive_router_config::{override_subgraph_urls::UrlOrExpression, HiveRouterConfig};
9+
use hive_router_config::{
10+
override_subgraph_urls::UrlOrExpression, traffic_shaping::DurationOrExpression,
11+
HiveRouterConfig,
12+
};
1013
use http::Uri;
1114
use hyper_tls::HttpsConnector;
1215
use hyper_util::{
@@ -41,6 +44,12 @@ type EndpointsBySubgraphMap = DashMap<SubgraphName, SubgraphEndpoint>;
4144
type ExpressionsBySubgraphMap = HashMap<SubgraphName, VrlProgram>;
4245
type TimeoutsBySubgraph = DashMap<SubgraphName, DurationOrProgram>;
4346

47+
struct ResolvedSubgraphConfig<'a> {
48+
client: Arc<HttpClient>,
49+
timeout_config: &'a DurationOrExpression,
50+
dedupe_enabled: bool,
51+
}
52+
4453
pub struct SubgraphExecutorMap {
4554
executors_by_subgraph: ExecutorsBySubgraphMap,
4655
/// Mapping from subgraph name to endpoint for quick lookup
@@ -317,53 +326,15 @@ impl SubgraphExecutorMap {
317326
.or_insert_with(|| Arc::new(Semaphore::new(self.max_connections_per_host)))
318327
.clone();
319328

320-
let mut client = self.client.clone();
321-
let mut timeout_config = &self.config.traffic_shaping.all.request_timeout;
322-
let pool_idle_timeout = self.config.traffic_shaping.all.pool_idle_timeout;
323-
let mut dedupe_enabled = self.config.traffic_shaping.all.dedupe_enabled;
324-
if let Some(subgraph_traffic_shaping_config) =
325-
self.config.traffic_shaping.subgraphs.get(subgraph_name)
326-
{
327-
if subgraph_traffic_shaping_config.pool_idle_timeout.is_some() {
328-
client = Arc::new(
329-
Client::builder(TokioExecutor::new())
330-
.pool_timer(TokioTimer::new())
331-
.pool_idle_timeout(
332-
subgraph_traffic_shaping_config
333-
.pool_idle_timeout
334-
.unwrap_or(pool_idle_timeout),
335-
)
336-
.pool_max_idle_per_host(self.max_connections_per_host)
337-
.build(HttpsConnector::new()),
338-
);
339-
}
340-
dedupe_enabled = subgraph_traffic_shaping_config
341-
.dedupe_enabled
342-
.unwrap_or(dedupe_enabled);
343-
timeout_config = subgraph_traffic_shaping_config
344-
.request_timeout
345-
.as_ref()
346-
.unwrap_or(timeout_config);
347-
}
348-
349-
if !self.timeouts_by_subgraph.contains_key(subgraph_name) {
350-
let timeout_prog: DurationOrProgram = compile_duration_expression(timeout_config, None)
351-
.map_err(|err| {
352-
SubgraphExecutorError::RequestTimeoutExpressionBuild(
353-
subgraph_name.to_string(),
354-
err,
355-
)
356-
})?;
357-
self.timeouts_by_subgraph
358-
.insert(subgraph_name.to_string(), timeout_prog);
359-
}
329+
let subgraph_config = self.resolve_subgraph_config(subgraph_name);
330+
self.register_timeout_if_absent(subgraph_name, &subgraph_config.timeout_config)?;
360331

361332
let executor = HTTPSubgraphExecutor::new(
362333
subgraph_name.to_string(),
363334
endpoint_uri,
364-
client,
335+
subgraph_config.client,
365336
semaphore,
366-
dedupe_enabled,
337+
subgraph_config.dedupe_enabled,
367338
self.in_flight_requests.clone(),
368339
);
369340

@@ -374,6 +345,62 @@ impl SubgraphExecutorMap {
374345

375346
Ok(())
376347
}
348+
349+
/// Resolves traffic shaping configuration for a specific subgraph, applying subgraph-specific
350+
/// overrides on top of global settings
351+
fn resolve_subgraph_config<'a>(&'a self, subgraph_name: &'a str) -> ResolvedSubgraphConfig<'a> {
352+
let mut config = ResolvedSubgraphConfig {
353+
client: self.client.clone(),
354+
timeout_config: &self.config.traffic_shaping.all.request_timeout,
355+
dedupe_enabled: self.config.traffic_shaping.all.dedupe_enabled,
356+
};
357+
358+
let Some(subgraph_config) = self.config.traffic_shaping.subgraphs.get(subgraph_name) else {
359+
return config;
360+
};
361+
362+
// Override client only if pool idle timeout is customized
363+
if let Some(pool_idle_timeout) = subgraph_config.pool_idle_timeout {
364+
config.client = Arc::new(
365+
Client::builder(TokioExecutor::new())
366+
.pool_timer(TokioTimer::new())
367+
.pool_idle_timeout(pool_idle_timeout)
368+
.pool_max_idle_per_host(self.max_connections_per_host)
369+
.build(HttpsConnector::new()),
370+
);
371+
}
372+
373+
// Apply other subgraph-specific overrides
374+
if let Some(dedupe_enabled) = subgraph_config.dedupe_enabled {
375+
config.dedupe_enabled = dedupe_enabled;
376+
}
377+
378+
if let Some(custom_timeout) = &subgraph_config.request_timeout {
379+
config.timeout_config = custom_timeout;
380+
}
381+
382+
config
383+
}
384+
385+
/// Compiles and caches the timeout configuration for the given subgraph if not already registered
386+
fn register_timeout_if_absent(
387+
&self,
388+
subgraph_name: &str,
389+
timeout_config: &hive_router_config::traffic_shaping::DurationOrExpression,
390+
) -> Result<(), SubgraphExecutorError> {
391+
if self.timeouts_by_subgraph.contains_key(subgraph_name) {
392+
return Ok(());
393+
}
394+
395+
let timeout_prog = compile_duration_expression(timeout_config, None).map_err(|err| {
396+
SubgraphExecutorError::RequestTimeoutExpressionBuild(subgraph_name.to_string(), err)
397+
})?;
398+
399+
self.timeouts_by_subgraph
400+
.insert(subgraph_name.to_string(), timeout_prog);
401+
402+
Ok(())
403+
}
377404
}
378405

379406
fn resolve_duration_prog(

lib/router-config/src/supergraph.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub enum SupergraphSource {
2323
deserialize_with = "humantime_serde::deserialize",
2424
serialize_with = "humantime_serde::serialize"
2525
)]
26+
#[schemars(with = "String")]
2627
poll_interval: Option<Duration>,
2728
},
2829
/// Loads a supergraph from Hive Console CDN.
@@ -44,20 +45,23 @@ pub enum SupergraphSource {
4445
deserialize_with = "humantime_serde::deserialize",
4546
serialize_with = "humantime_serde::serialize"
4647
)]
48+
#[schemars(with = "String")]
4749
poll_interval: Duration,
4850
/// Request timeout for the Hive Console CDN requests.
4951
#[serde(
5052
default = "default_hive_request_timeout",
5153
deserialize_with = "humantime_serde::deserialize",
5254
serialize_with = "humantime_serde::serialize"
5355
)]
56+
#[schemars(with = "String")]
5457
request_timeout: Duration,
5558
/// Connect timeout for the Hive Console CDN requests.
5659
#[serde(
5760
default = "default_hive_connect_timeout",
5861
deserialize_with = "humantime_serde::deserialize",
5962
serialize_with = "humantime_serde::serialize"
6063
)]
64+
#[schemars(with = "String")]
6165
connect_timeout: Duration,
6266
/// Whether to accept invalid TLS certificates when connecting to the Hive Console CDN.
6367
#[serde(default = "default_accept_invalid_certs")]

lib/router-config/src/traffic_shaping.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
88
pub struct TrafficShapingConfig {
99
/// The default configuration that will be applied to all subgraphs, unless overridden by a specific subgraph configuration.
1010
#[serde(default)]
11-
pub all: TrafficShapingExecutorConfig,
11+
pub all: TrafficShapingExecutorGlobalConfig,
1212
/// Optional per-subgraph configurations that will override the default configuration for specific subgraphs.
1313
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
1414
pub subgraphs: HashMap<String, TrafficShapingExecutorSubgraphConfig>,
@@ -20,7 +20,7 @@ pub struct TrafficShapingConfig {
2020
impl Default for TrafficShapingConfig {
2121
fn default() -> Self {
2222
Self {
23-
all: TrafficShapingExecutorConfig::default(),
23+
all: TrafficShapingExecutorGlobalConfig::default(),
2424
subgraphs: HashMap::new(),
2525
max_connections_per_host: default_max_connections_per_host(),
2626
}
@@ -79,7 +79,7 @@ pub struct TrafficShapingExecutorSubgraphConfig {
7979

8080
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)]
8181
#[serde(deny_unknown_fields)]
82-
pub struct TrafficShapingExecutorConfig {
82+
pub struct TrafficShapingExecutorGlobalConfig {
8383
/// Timeout for idle sockets being kept-alive.
8484
#[serde(
8585
default = "default_pool_idle_timeout",
@@ -130,12 +130,13 @@ pub enum DurationOrExpression {
130130
deserialize_with = "humantime_serde::deserialize",
131131
serialize_with = "humantime_serde::serialize"
132132
)]
133+
#[schemars(with = "String")]
133134
Duration(Duration),
134135
/// A VRL expression that evaluates to a duration. The result can be an integer (milliseconds), a float (milliseconds), or a duration string (e.g. "5s").
135136
Expression { expression: String },
136137
}
137138

138-
impl Default for TrafficShapingExecutorConfig {
139+
impl Default for TrafficShapingExecutorGlobalConfig {
139140
fn default() -> Self {
140141
Self {
141142
pool_idle_timeout: default_pool_idle_timeout(),

0 commit comments

Comments
 (0)