agave_validator/commands/exit/
mod.rs1#[cfg(target_os = "linux")]
2use std::{io, thread, time::Duration};
3use {
4 crate::{
5 admin_rpc_service,
6 commands::{Error, FromClapArgMatches, Result, wait_for_restart_window},
7 },
8 clap::{App, Arg, ArgMatches, SubCommand, value_t_or_exit},
9 solana_clap_utils::input_validators::{is_parsable, is_valid_percentage},
10 std::path::Path,
11};
12
13const COMMAND: &str = "exit";
14
15const DEFAULT_MIN_IDLE_TIME: &str = "10";
16const DEFAULT_MAX_DELINQUENT_STAKE: &str = "5";
17
18#[derive(Debug, PartialEq)]
19pub struct ExitArgs {
20 pub force: bool,
21 pub wait_for_exit: bool,
22 pub min_idle_time: usize,
23 pub max_delinquent_stake: u8,
24 pub skip_new_snapshot_check: bool,
25 pub skip_health_check: bool,
26}
27
28impl FromClapArgMatches for ExitArgs {
29 fn from_clap_arg_match(matches: &ArgMatches) -> Result<Self> {
30 Ok(ExitArgs {
31 force: matches.is_present("force"),
32 wait_for_exit: !matches.is_present("no_wait_for_exit"),
33 min_idle_time: value_t_or_exit!(matches, "min_idle_time", usize),
34 max_delinquent_stake: value_t_or_exit!(matches, "max_delinquent_stake", u8),
35 skip_new_snapshot_check: matches.is_present("skip_new_snapshot_check"),
36 skip_health_check: matches.is_present("skip_health_check"),
37 })
38 }
39}
40
41pub fn command<'a>() -> App<'a, 'a> {
42 SubCommand::with_name(COMMAND)
43 .about("Send an exit request to the validator")
44 .arg(
45 Arg::with_name("force")
46 .short("f")
47 .long("force")
48 .takes_value(false)
49 .help(
50 "Request the validator exit immediately instead of waiting for a restart \
51 window",
52 ),
53 )
54 .arg(
55 Arg::with_name("no_wait_for_exit")
56 .long("no-wait-for-exit")
57 .takes_value(false)
58 .conflicts_with("wait_for_exit")
59 .help("Do not wait for the validator to terminate after sending the exit request"),
60 )
61 .arg(
62 Arg::with_name("min_idle_time")
63 .long("min-idle-time")
64 .takes_value(true)
65 .validator(is_parsable::<usize>)
66 .value_name("MINUTES")
67 .default_value(DEFAULT_MIN_IDLE_TIME)
68 .help("Minimum time that the validator should not be leader before restarting"),
69 )
70 .arg(
71 Arg::with_name("max_delinquent_stake")
72 .long("max-delinquent-stake")
73 .takes_value(true)
74 .validator(is_valid_percentage)
75 .default_value(DEFAULT_MAX_DELINQUENT_STAKE)
76 .value_name("PERCENT")
77 .help("The maximum delinquent stake % permitted for an exit"),
78 )
79 .arg(
80 Arg::with_name("skip_new_snapshot_check")
81 .long("skip-new-snapshot-check")
82 .help("Skip check for a new snapshot"),
83 )
84 .arg(
85 Arg::with_name("skip_health_check")
86 .long("skip-health-check")
87 .help("Skip health check"),
88 )
89}
90
91pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> {
92 let exit_args = ExitArgs::from_clap_arg_match(matches)?;
93
94 if !exit_args.force {
95 wait_for_restart_window::wait_for_restart_window(
96 ledger_path,
97 None,
98 exit_args.min_idle_time,
99 exit_args.max_delinquent_stake,
100 exit_args.skip_new_snapshot_check,
101 exit_args.skip_health_check,
102 )?;
103 }
104
105 const WAIT_FOR_EXIT_UNSUPPORTED_ERROR: &str = "remote process exit cannot be waited on. \
112 `--wait-for-exit` is not supported by the \
113 remote process";
114 let validator_pid = admin_rpc_service::runtime().block_on(async move {
115 let admin_client = admin_rpc_service::connect(ledger_path).await?;
116 let validator_pid = if exit_args.wait_for_exit {
117 admin_client
118 .pid()
119 .await
120 .map_err(|_err| Error::Dynamic(WAIT_FOR_EXIT_UNSUPPORTED_ERROR.into()))?
121 } else {
122 0
123 };
124 admin_client.exit().await?;
125
126 Ok::<u32, Error>(validator_pid)
127 })?;
128
129 println!("Exit request sent");
130 if exit_args.wait_for_exit {
131 poll_until_pid_terminates(validator_pid)?;
132 }
133
134 Ok(())
135}
136
137#[cfg(target_os = "linux")]
138fn poll_until_pid_terminates(pid: u32) -> Result<()> {
139 let pid = i32::try_from(pid)?;
140
141 println!("Waiting for agave-validator process {pid} to terminate");
142 loop {
143 let result = unsafe {
150 libc::kill(pid, 0)
151 };
152 if result >= 0 {
153 thread::sleep(Duration::from_millis(500));
155 } else {
156 let errno = io::Error::last_os_error()
157 .raw_os_error()
158 .ok_or(Error::Dynamic("unable to read raw os error".into()))?;
159 match errno {
160 libc::ESRCH => {
161 println!("Done, agave-validator process {pid} has terminated");
162 break;
163 }
164 libc::EINVAL => {
165 Err(Error::Dynamic(
168 format!("unexpected invalid signal error for kill({pid}, 0)").into(),
169 ))?;
170 }
171 libc::EPERM => {
172 Err(io::Error::from(io::ErrorKind::PermissionDenied))?;
173 }
174 unknown => {
175 Err(Error::Dynamic(
176 format!("unexpected errno for kill({pid}, 0): {unknown}").into(),
177 ))?;
178 }
179 }
180 }
181 }
182
183 Ok(())
184}
185
186#[cfg(not(target_os = "linux"))]
187fn poll_until_pid_terminates(_pid: u32) -> Result<()> {
188 Err(Error::Dynamic(
189 "Unable to wait for agave-validator process termination on this platform".into(),
190 ))
191}
192
193#[cfg(test)]
194mod tests {
195 use {super::*, crate::commands::tests::verify_args_struct_by_command};
196
197 impl Default for ExitArgs {
198 fn default() -> Self {
199 ExitArgs {
200 min_idle_time: DEFAULT_MIN_IDLE_TIME
201 .parse()
202 .expect("invalid DEFAULT_MIN_IDLE_TIME"),
203 max_delinquent_stake: DEFAULT_MAX_DELINQUENT_STAKE
204 .parse()
205 .expect("invalid DEFAULT_MAX_DELINQUENT_STAKE"),
206 force: false,
207 wait_for_exit: true,
208 skip_new_snapshot_check: false,
209 skip_health_check: false,
210 }
211 }
212 }
213
214 #[test]
215 fn verify_args_struct_by_command_exit_default() {
216 verify_args_struct_by_command(command(), vec![COMMAND], ExitArgs::default());
217 }
218
219 #[test]
220 fn verify_args_struct_by_command_exit_with_force() {
221 verify_args_struct_by_command(
222 command(),
223 vec![COMMAND, "--force"],
224 ExitArgs {
225 force: true,
226 ..ExitArgs::default()
227 },
228 );
229 }
230
231 #[test]
232 fn verify_args_struct_by_command_exit_wait_for_exit() {
233 verify_args_struct_by_command(
234 command(),
235 vec![COMMAND, "--no-wait-for-exit"],
236 ExitArgs {
237 wait_for_exit: false,
238 ..ExitArgs::default()
239 },
240 );
241 }
242
243 #[test]
244 fn verify_args_struct_by_command_exit_with_min_idle_time() {
245 verify_args_struct_by_command(
246 command(),
247 vec![COMMAND, "--min-idle-time", "60"],
248 ExitArgs {
249 min_idle_time: 60,
250 ..ExitArgs::default()
251 },
252 );
253 }
254
255 #[test]
256 fn verify_args_struct_by_command_exit_with_max_delinquent_stake() {
257 verify_args_struct_by_command(
258 command(),
259 vec![COMMAND, "--max-delinquent-stake", "10"],
260 ExitArgs {
261 max_delinquent_stake: 10,
262 ..ExitArgs::default()
263 },
264 );
265 }
266
267 #[test]
268 fn verify_args_struct_by_command_exit_with_skip_new_snapshot_check() {
269 verify_args_struct_by_command(
270 command(),
271 vec![COMMAND, "--skip-new-snapshot-check"],
272 ExitArgs {
273 skip_new_snapshot_check: true,
274 ..ExitArgs::default()
275 },
276 );
277 }
278
279 #[test]
280 fn verify_args_struct_by_command_exit_with_skip_health_check() {
281 verify_args_struct_by_command(
282 command(),
283 vec![COMMAND, "--skip-health-check"],
284 ExitArgs {
285 skip_health_check: true,
286 ..ExitArgs::default()
287 },
288 );
289 }
290}