Skip to main content

agave_validator/commands/exit/
mod.rs

1#[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    // Grab the pid from the process before initiating exit as the running
106    // validator will be unable to respond after exit has returned.
107    //
108    // Additionally, only check the pid() RPC call result if it will be used.
109    // In an upgrade scenario, it is possible that a binary that calls pid()
110    // will be initiating exit against a process that doesn't support pid().
111    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        // From man kill(2)
144        //
145        // If sig is 0, then no signal is sent, but existence and permission
146        // checks are still performed; this can be used to check for the
147        // existence of a process ID or process group ID that the caller is
148        // permitted to signal.
149        let result = unsafe {
150            libc::kill(pid, /*sig:*/ 0)
151        };
152        if result >= 0 {
153            // Give the process some time to exit before checking again
154            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                    // An invalid signal was specified, we only pass sig=0 so
166                    // this should not be possible
167                    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}