Repository navigation
Add supervised restart support to Gateway backend #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| //! Apply keeps rollback copies: the first Apply since the last start copies | ||
| //! each real file it replaces to `<file>.prev`, and later Applies keep that | ||
| //! copy; an env-only change that breaks a `${VAR}` is refused. | ||
|
|
||
| use gateway_config::write_shadow; | ||
|
|
||
| use super::{CONFIG, GAMMA_MODEL, fixture, post, serve_fixture, stage_gamma}; | ||
| use crate::commands::apply::capture_apply; | ||
| use crate::config_shadow::rollback_path; | ||
|
|
||
| #[tokio::test] | ||
| async fn apply_keeps_the_booted_config_as_the_rollback_copy() { | ||
| let (_temp, config, paths) = fixture(); | ||
| let config_path = paths.config_path.clone(); | ||
| stage_gamma(&config_path); | ||
| let (addr, _state) = serve_fixture(config, paths).await; | ||
|
|
||
| let response = post(addr, "admin/config-apply").await; | ||
|
|
||
| assert_eq!(response.status(), reqwest::StatusCode::OK); | ||
| assert_eq!( | ||
| std::fs::read_to_string(rollback_path(&config_path)) | ||
| .expect("should be able to read the rollback copy"), | ||
| CONFIG, | ||
| "the rollback config should have booted" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn a_second_apply_keeps_the_first_rollback_copy() { | ||
| let (_temp, config, paths) = fixture(); | ||
|
|
||
| let config_path = paths.config_path.clone(); | ||
| stage_gamma(&config_path); | ||
|
|
||
| let (addr, _state) = serve_fixture(config, paths).await; | ||
| post(addr, "admin/config-apply").await; | ||
|
|
||
| let edited = format!("{CONFIG}{GAMMA_MODEL}") | ||
| .replace("description = \"gamma\"", "description = \"gamma, edited\""); | ||
|
|
||
| write_shadow(&config_path, &edited).expect("the second config change should succeed"); | ||
|
|
||
| let response = post(addr, "admin/config-apply").await; | ||
|
|
||
| assert_eq!(response.status(), reqwest::StatusCode::OK); | ||
| assert_eq!( | ||
| std::fs::read_to_string(rollback_path(&config_path)) | ||
| .expect("the rollback copy should be successfully read"), | ||
| CONFIG, | ||
| "a second call to Apply should not change the contents of the rollback file" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn an_env_change_that_breaks_a_reference_is_refused() { | ||
| let temp = tempfile::TempDir::new().expect("temp dir"); | ||
| let config_path = temp.path().join("gateway.toml"); | ||
|
|
||
| std::fs::write( | ||
| &config_path, | ||
| "config-version = 0\n\n[server]\nbind = \"127.0.0.1:0\"\n\ | ||
| api_key = \"${PF_TEST_ROLLBACK_KEY}\"\n", | ||
| ) | ||
| .expect("write config"); | ||
|
|
||
| let env_path = config_path.with_extension("env"); | ||
|
|
||
| std::fs::write(&env_path, "PF_TEST_ROLLBACK_KEY=booted\n") | ||
| .expect("writing env file should succeed"); | ||
|
|
||
| write_shadow(&env_path, "OTHER=value\n").expect("writing to shadow should succeed"); | ||
|
|
||
| let result = capture_apply(&config_path); | ||
|
|
||
| assert!( | ||
| result.is_err(), | ||
| "an env-only change must resolve every reference the config makes" | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,8 +34,8 @@ use tokio_util::sync::CancellationToken; | |
| use crate::AppState; | ||
| use crate::auth::LoopbackCaller; | ||
| use crate::commands::Command; | ||
| use crate::commands::apply::{ApplyPlan, capture_apply, promote_captures}; | ||
| use crate::config_shadow::{config_root, relative_name, shadow_census}; | ||
| use crate::commands::apply::{ApplyPlan, capture_apply, promote_captures, write_rollback_files}; | ||
| use crate::config_shadow::{ShadowCensus, config_root, relative_name}; | ||
| use crate::error::error_chain; | ||
| use crate::error::{GatewayError, blocking}; | ||
| use crate::registry::RouteInfo; | ||
|
|
@@ -110,14 +110,22 @@ async fn admin_config_apply( | |
| files, | ||
| restart_required, | ||
| } => { | ||
| let applied = blocking(move || promote_captures(&files)).await??; | ||
| let applied = blocking(move || { | ||
| write_rollback_files(&files)?; | ||
| promote_captures(&files) | ||
| }) | ||
| .await??; | ||
| return Ok(Json(ApplyReply { | ||
| applied, | ||
| reloaded: false, | ||
| restart_required, | ||
| })); | ||
| } | ||
| ApplyPlan::Reload(snapshot) => snapshot, | ||
| ApplyPlan::Reload(snapshot) => { | ||
| let captures = snapshot.captures().to_vec(); | ||
| blocking(move || write_rollback_files(&captures)).await??; | ||
| snapshot | ||
| } | ||
| }; | ||
| let applied = snapshot.applied_names(); | ||
| let restart_required = snapshot.restart_required; | ||
|
Comment on lines
110
to
131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code block should be a re-usable function that accepts I know it's something you just added to and didn't create yourself. It would make that entire code block easier to test independent of the remaining code in the let config_path = crate::admin::config_path(&state)?.to_path_buf();
let (enqueued, applied, restart_required) = {
// The lock spans the census, the parse, and the capture (or the
// inline promotion), so a save cannot land between them and the
// snapshot is one the latest save validated whole. It is released
// before the command runs: the queue serializes the reload itself.
let _guard = state.apply.lock().await;
let plan = blocking(move || capture_apply(&config_path)).await??;
let snapshot = match plan {
ApplyPlan::Inline {
files,
restart_required,
} => {
let applied = blocking(move || promote_captures(&files)).await??;
return Ok(Json(ApplyReply {
applied,
reloaded: false,
restart_required,
}));
}
ApplyPlan::Reload(snapshot) => snapshot,
};
let restart_required = snapshot.restart_required;
let enqueued = state.commands.enqueue(Command::ApplyConfig {
snapshot,
token: CancellationToken::new(),
});
(enqueued, snapshot.applied_names(), restart_required)
};
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's not a bad idea, but I think to make that a useful change we would also have to write the tests, and I think it should be a separate PR |
||
|
|
@@ -174,10 +182,10 @@ async fn admin_config_revert( | |
| /// Deletes every shadow the census finds, returning the deleted shadow | ||
| /// files relative to the config root, sorted. | ||
| fn delete_all_shadows(config_path: &Path) -> Result<Vec<String>, GatewayError> { | ||
| let census = shadow_census(config_path)?; | ||
| let census = ShadowCensus::new(config_path)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idiomatic change using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wtf is a ShadowCensus lol
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was written by you @vinniefalco ! pub(crate) struct ShadowCensus {
/// Real files whose shadows exist, in canonical form, without duplicates.
pub(crate) files: Vec<PathBuf>,
/// Top-level sections whose merged value the shadows change, sorted and deduplicated.
pub(crate) sections: Vec<String>,
}Looks like it's related to the configuration files and the individual sections. Probably not the best name though.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remind me of a Shadow Council.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something about lizard people controlling the government... I agree it's a terrible name. It tracks changes that are being written to the |
||
| let root = config_root(config_path); | ||
| let mut reverted: Vec<String> = Vec::with_capacity(census.files.len()); | ||
| for file in &census.files { | ||
| let mut reverted: Vec<String> = Vec::with_capacity(census.files().len()); | ||
| for file in census.files() { | ||
| let shadow: PathBuf = shadow_path(file); | ||
| std::fs::remove_file(&shadow) | ||
| .map_err(|source| GatewayError::ConfigWriteIo(Box::new(source)))?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we had to move this over to using an Arc?
Is there one created on server start and passed down into all callers type of thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is created on boot and them passed to multiple threads. Personally I think an mpsc::channel would make more sense here, but I didn't want to re-work the existing shutdown code (yet).