1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! `aion server restart`: hand this home over from the running server to a
//! fresh one of the SAME binary, and narrate both halves until it serves.
//!
//! The handover itself lives in [`crate::handover`], because `aion update`
//! performs exactly the same one with a newly installed binary. What is here
//! is this verb's own surface: which executable starts (this one), and how the
//! drain-side patience is resolved.
//!
//! Exit codes: 0 when the successor is serving; 1 when the handover was
//! attempted and did not complete (the successor died, or the predecessor
//! would not let go); 2 for a refusal before anything was signalled.
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use aion_server::config::aion_home;
use aion_server::control::PidRecord;
use clap::Args;
use crate::handover::{self, HandoverRefusal};
/// Arguments for `aion server restart`.
#[derive(Args, Clone, Debug)]
pub struct RestartArgs {
/// Path to the TOML server configuration file. Used only to resolve the
/// drain patience when the running server's record carries none.
#[arg(long)]
pub config: Option<PathBuf>,
/// Seconds to wait for the running server to begin draining before giving
/// up on the handover. Absent, the running server's own recorded drain
/// window governs, then the config's `drain.timeout_seconds` — the verb
/// never invents a value.
#[arg(long)]
pub patience: Option<NonZeroU64>,
}
/// Run `aion server restart`.
pub async fn run(args: &RestartArgs) -> ExitCode {
match restart(args).await {
Ok(code) => code,
Err(refusal) => {
eprintln!("aion server restart: {}", refusal.message);
ExitCode::from(refusal.code)
}
}
}
async fn restart(args: &RestartArgs) -> Result<ExitCode, HandoverRefusal> {
let home = aion_home()
.map_err(|error| {
HandoverRefusal::refused(format!("could not resolve the Aion home: {error}"))
})?
.path;
let predecessor = handover::resolve_predecessor(&home, "restart")?;
let patience = resolve_patience(args.patience, args.config.as_deref(), &predecessor)?;
handover::release_predecessor(&home, &predecessor, patience)?;
let executable = std::env::current_exe().map_err(|error| {
HandoverRefusal::incomplete(format!(
"the previous server was signalled but this binary's own path could not be \
resolved to start its successor: {error}"
))
})?;
let _successor = handover::start_successor(&home, &executable, Some(&predecessor)).await?;
Ok(ExitCode::SUCCESS)
}
/// The drain-side patience: the flag, then the running server's own recorded
/// window, then the config's `drain.timeout_seconds`. Never invented — the
/// same precedence `aion server stop` uses, and for the same reason: the
/// window the server is ACTUALLY running with may have come from its own
/// command line, and two verbs waiting out different windows on one server
/// would report different things about the same drain.
///
/// # Errors
///
/// Returns [`HandoverRefusal::refused`] when no window can be resolved from
/// any of the three sources — nothing is signalled without one to report on.
pub fn resolve_patience(
flag: Option<NonZeroU64>,
config_path: Option<&std::path::Path>,
predecessor: &PidRecord,
) -> Result<Duration, HandoverRefusal> {
if let Some(seconds) = flag {
return Ok(Duration::from_secs(seconds.get()));
}
if predecessor.drain_timeout_seconds > 0 {
return Ok(Duration::from_secs(predecessor.drain_timeout_seconds));
}
let overrides = aion_server::config::CliOverrides {
config_path: config_path.map(std::path::Path::to_path_buf),
..aion_server::config::CliOverrides::default()
};
match aion_server::config::ServerConfig::load(&overrides) {
Ok(config) => Ok(Duration::from_secs(config.drain.timeout_seconds)),
Err(error) => Err(HandoverRefusal::refused(format!(
"the running server records no drain window and the configuration could \
not be read to supply one ({error}); nothing was signalled. Pass \
`--patience <seconds>` to rule it at invocation"
))),
}
}
#[cfg(test)]
mod tests {
use super::resolve_patience;
use aion_server::control::{IncarnationState, PidRecord};
use std::num::NonZeroU64;
use std::time::Duration;
fn serving(drain_timeout_seconds: u64) -> PidRecord {
PidRecord {
pid: 1234,
started_at_unix_secs: 1,
binary_sha256: "0".repeat(64),
version: "0.0.0-test".to_owned(),
commit: "test".to_owned(),
state: IncarnationState::Serving,
http_address: None,
grpc_address: None,
intended_http_address: None,
intended_grpc_address: None,
stage: None,
stage_detail: None,
stage_seq: 0,
stage_updated_at_unix_secs: 0,
drain_timeout_seconds,
}
}
/// The flag outranks the record's window, and the record's window
/// outranks the config — the same precedence `stop` uses, so the two
/// verbs cannot wait out different windows on the same server.
#[test]
fn the_drain_patience_prefers_the_flag_then_the_record()
-> Result<(), Box<dyn std::error::Error>> {
let record = serving(300);
let patience = resolve_patience(NonZeroU64::new(7), None, &record)
.map_err(|refusal| refusal.message.clone())?;
assert_eq!(patience, Duration::from_secs(7));
let patience =
resolve_patience(None, None, &record).map_err(|refusal| refusal.message.clone())?;
assert_eq!(
patience,
Duration::from_secs(300),
"the running server's own recorded window must govern"
);
Ok(())
}
/// A BOOTING record carries no window (it has not resolved one yet), and
/// a record that predates the field reads as 0. Both honestly mean "no
/// recorded window" and must fall through rather than resolve to zero,
/// which would give up on the drain instantly.
#[test]
fn a_zero_window_falls_through_rather_than_governing() {
let record = serving(0);
// With no config file discoverable in the test environment this may
// resolve from an ambient config or refuse; either is correct. What
// must NOT happen is a zero-second patience.
match resolve_patience(None, None, &record) {
Ok(patience) => assert_ne!(
patience,
Duration::ZERO,
"a zero recorded window must never become a zero patience"
),
Err(refusal) => assert_eq!(refusal.code, 2),
}
}
}