Skip to content

Commit 3ab8d3f

Browse files
authored
feat: rover dev listens for .env file changes and hot reloads router (#2984)
<!-- First, 🌠 thank you 🌠 for taking the time to consider a contribution to Apollo! Here are some important details to follow: * ⏰ Your time is important To save your precious time, if the contribution you are making will take more than an hour, please make sure it has been discussed in an issue first. This is especially true for feature requests! * 💡 Features Feature requests can be created and discussed within a GitHub Issue. Be sure to search for existing feature requests (and related issues!) prior to opening a new request. If an existing issue covers the need, please upvote that issue by using the 👍 emote, rather than opening a new issue. * 🕷 Bug fixes These can be created and discussed in this repository. When fixing a bug, please _try_ to add a test which verifies the fix. If you cannot, you should still submit the PR but we may still ask you (and help you!) to create a test. * 📖 Contribution guidelines Follow https://github.com/apollographql/rover/blob/HEAD/CONTRIBUTING.md when submitting a pull request. Make sure existing tests still pass, and add tests for all new behavior. * ✏️ Explain your pull request Describe the big picture of your changes here to communicate to what your pull request is meant to accomplish. Provide 🔗 links 🔗 to associated issues! We hope you will find this to be a positive experience! Open source contribution can be intimidating and we hope to alleviate that pain as much as possible. Without following these guidelines, you may be missing context that can help you succeed with your contribution, which is why we encourage discussion first. Ultimately, there is no guarantee that we will be able to merge your pull-request, but by following these guidelines we can try to avoid disappointment. --> Fix for: #2750 - Adds a file watcher for `.env` - Rereads the config file and hot reloads the router on `.env` file changes ### Testing **Setup**: `router.yaml` ``` supergraph: listen: 0.0.0.0:${env.ROUTER_PORT} ``` `.env` ``` ROUTER_PORT=4000 ``` **Run `rover dev`** ``` /rover dev --graph-ref my-test@current --router-config router.yaml retrieving subgraphs remotely from my-test@current merging supergraph schema files starting a session with the 'starwars' subgraph composing supergraph with Federation 2.12.2 ==> Watching router.yaml for changes ==> Attempting to start router at http://localhost:4000. ==> Health check exposed at http://127.0.0.1:8088/health WARN: Connector debugging is enabled, this may expose sensitive information. ==> Your supergraph is running! head to http://localhost:4000 to query your supergraph ``` **Change the `.env` file by updating the router port to 4002** ``` DotEnvWatcher: .env file changed ==> Router config updated. supergraph: listen: 0.0.0.0:4002 ==> Health check exposed at http://127.0.0.1:8088/health ==> Your supergraph is running! head to http://localhost:4002 to query your supergraph ```
1 parent 6be0cd1 commit 3ab8d3f

File tree

3 files changed

+148
-7
lines changed

3 files changed

+148
-7
lines changed

src/command/dev/router/run.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use super::{
2020
config::{ReadRouterConfigError, RouterAddress, RunRouterConfig, remote::RemoteRouterConfig},
2121
hot_reload::{HotReloadEvent, HotReloadWatcher, RouterUpdateEvent},
2222
install::{InstallRouter, InstallRouterError},
23-
watchers::router_config::RouterConfigWatcher,
23+
watchers::{
24+
dot_env::{DotEnvEvent, DotEnvReload, DotEnvWatcher},
25+
router_config::RouterConfigWatcher,
26+
},
2427
};
2528
use crate::{
2629
RoverError,
@@ -380,7 +383,7 @@ impl RunRouter<state::Watch> {
380383
{
381384
tracing::info!("Watching for subgraph changes");
382385
let (router_config_updates, config_watcher_subtask) =
383-
if let Some(config_path) = self.state.config_path {
386+
if let Some(config_path) = self.state.config_path.clone() {
384387
let config_watcher = RouterConfigWatcher::new(FileWatcher::new(config_path));
385388
let (events, subtask): (UnboundedReceiverStream<RouterUpdateEvent>, _) =
386389
Subtask::new(config_watcher);
@@ -389,6 +392,16 @@ impl RunRouter<state::Watch> {
389392
(None, None)
390393
};
391394

395+
let (dot_env_updates, dot_env_watcher_subtask) = if dotenvy::dotenv().is_ok() {
396+
tracing::info!("Watching for .env changes");
397+
let dot_env_watcher = DotEnvWatcher::new();
398+
let (events, subtask): (UnboundedReceiverStream<DotEnvEvent>, _) =
399+
Subtask::new(dot_env_watcher);
400+
(Some(events), Some(subtask))
401+
} else {
402+
(None, None)
403+
};
404+
392405
let composition_messages =
393406
tokio_stream::StreamExt::filter_map(composition_messages, |event| match event {
394407
CompositionEvent::Error(CompositionError::Build { source, .. }) => {
@@ -421,11 +434,33 @@ impl RunRouter<state::Watch> {
421434

422435
let (hot_reload_events, hot_reload_subtask): (UnboundedReceiverStream<HotReloadEvent>, _) =
423436
Subtask::new(hot_reload_watcher);
424-
let router_config_updates = router_config_updates
425-
.map(move |stream| stream.boxed())
426-
.unwrap_or_else(|| stream::empty().boxed());
427-
let router_updates =
428-
tokio_stream::StreamExt::merge(router_config_updates, composition_messages);
437+
438+
let mut streams: Vec<_> = vec![];
439+
streams.push(composition_messages.boxed());
440+
441+
if let Some(stream) = router_config_updates {
442+
streams.push(stream.boxed());
443+
}
444+
445+
if let (Some(dot_env_event_stream), Some(config_path)) =
446+
(dot_env_updates, self.state.config_path.clone())
447+
{
448+
// Emit RouterUpdateEvent::ConfigChanged for each .env change
449+
let dot_env_reload = DotEnvReload {
450+
router_config_path: config_path,
451+
};
452+
let (router_config_updates, dot_env_reload_subtask): (
453+
UnboundedReceiverStream<RouterUpdateEvent>,
454+
_,
455+
) = Subtask::new(dot_env_reload);
456+
dot_env_reload_subtask.run(
457+
dot_env_event_stream.boxed(),
458+
Some(self.state.cancellation_token.clone()),
459+
);
460+
streams.push(router_config_updates.boxed());
461+
}
462+
463+
let router_updates = stream::select_all(streams).boxed();
429464

430465
SubtaskRunStream::run(
431466
hot_reload_subtask,
@@ -436,6 +471,9 @@ impl RunRouter<state::Watch> {
436471
if let Some(subtask) = config_watcher_subtask {
437472
subtask.run(Some(self.state.cancellation_token.clone()))
438473
}
474+
if let Some(subtask) = dot_env_watcher_subtask {
475+
subtask.run(Some(self.state.cancellation_token.clone()))
476+
}
439477

440478
RunRouter {
441479
state: state::Abort {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use camino::Utf8PathBuf;
2+
use dotenvy::dotenv_override;
3+
use futures::{StreamExt, stream::BoxStream};
4+
use rover_std::Fs;
5+
use tap::TapFallible;
6+
use tokio::sync::mpsc::UnboundedSender;
7+
use tokio_util::sync::CancellationToken;
8+
9+
use crate::{
10+
command::dev::router::{
11+
config::RouterConfig, hot_reload::RouterUpdateEvent, watchers::file::FileWatcher,
12+
},
13+
subtask::{SubtaskHandleStream, SubtaskHandleUnit},
14+
};
15+
16+
pub struct DotEnvWatcher {
17+
file_watcher: FileWatcher,
18+
}
19+
20+
#[derive(Debug)]
21+
pub enum DotEnvEvent {
22+
DotEnvChanged,
23+
}
24+
25+
impl DotEnvWatcher {
26+
pub fn new() -> Self {
27+
let dot_env_path = dotenvy::dotenv()
28+
.ok()
29+
.and_then(|p| Utf8PathBuf::from_path_buf(p).ok())
30+
.unwrap_or_else(|| Utf8PathBuf::from(".env"));
31+
32+
Self {
33+
file_watcher: FileWatcher::new(dot_env_path),
34+
}
35+
}
36+
}
37+
38+
impl SubtaskHandleUnit for DotEnvWatcher {
39+
type Output = DotEnvEvent;
40+
fn handle(
41+
self,
42+
sender: tokio::sync::mpsc::UnboundedSender<Self::Output>,
43+
cancellation_token: Option<CancellationToken>,
44+
) {
45+
let cancellation_token = cancellation_token.unwrap_or_default();
46+
tokio::spawn(async move {
47+
cancellation_token
48+
.run_until_cancelled(async move {
49+
// Emit a DotEnvEvent::Changed event which will trigger a router config reload
50+
while let Some(_env_contents) = self.file_watcher.clone().watch().next().await {
51+
let _ = sender
52+
.send(DotEnvEvent::DotEnvChanged)
53+
.tap_err(|err| tracing::error!("{:?}", err));
54+
}
55+
})
56+
.await;
57+
});
58+
}
59+
}
60+
61+
pub struct DotEnvReload {
62+
pub router_config_path: Utf8PathBuf,
63+
}
64+
65+
impl SubtaskHandleStream for DotEnvReload {
66+
type Input = DotEnvEvent;
67+
68+
type Output = RouterUpdateEvent;
69+
70+
fn handle(
71+
self,
72+
sender: UnboundedSender<Self::Output>,
73+
mut input: BoxStream<'static, Self::Input>,
74+
cancellation_token: Option<CancellationToken>,
75+
) {
76+
let cancellation_token = cancellation_token.unwrap_or_default();
77+
tokio::spawn(async move {
78+
cancellation_token
79+
.run_until_cancelled(async move {
80+
while let Some(_evt) = input.next().await {
81+
dotenv_override().ok();
82+
match Fs::read_file(self.router_config_path.clone()) {
83+
Ok(config_contents) => {
84+
let _ = sender
85+
.send(RouterUpdateEvent::ConfigChanged {
86+
config: RouterConfig::new(config_contents),
87+
})
88+
.tap_err(|err| tracing::error!("{:?}", err));
89+
}
90+
Err(err) => {
91+
tracing::error!(
92+
"Could not read router config after .env change: {:?}",
93+
err
94+
);
95+
}
96+
}
97+
}
98+
})
99+
.await;
100+
});
101+
}
102+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod dot_env;
12
pub mod file;
23
pub mod router_config;

0 commit comments

Comments
 (0)