eb 0.2.1

A command executor exercising exponential backoff
use clap::{App, AppSettings};
use rand::distributions::{Distribution, Uniform};
use std::process::Command;
use std::process::ExitStatus;
use std::thread::sleep;
use std::time::Duration;
use std::time::Instant;

pub enum SlotTime {
	UserSpecified(Duration),
	AutoGenerated(Duration),
}

// This function is a basic implementation of the generic from the
// `core::cmp::Ord` clamp function, which is unstable.  This code is stable,
// but eventually you might want to change this to the `.clamp` method on
// ord-y things.
pub fn clamp<T>(value: T, min: T, max: T) -> T
where
	T: core::cmp::PartialOrd,
{
	assert!(min <= max);
	if value < min {
		min
	} else if value > max {
		max
	} else {
		value
	}
}

fn main() {
	let max_n: u32 = 10;

	let matches = App::new(env!("CARGO_PKG_NAME"))
		.about(env!("CARGO_PKG_DESCRIPTION"))
		.version(env!("CARGO_PKG_VERSION"))
		.author(env!("CARGO_PKG_AUTHORS"))
		.setting(AppSettings::AllowExternalSubcommands)
		.get_matches();

	let mut command: Command = match matches.subcommand() {
		(name, Some(matches)) => {
			let mut command = Command::new(name);
			let args: Vec<&str> = matches.values_of("").map_or(Vec::new(), Iterator::collect);

			for arg in args {
				command.arg(arg);
			}

			command
		}
		_ => panic!("no command was given"),
	};

	let mut iterations: u32 = 0;
	let mut slot_time: Option<SlotTime> = None;
	let mut rng = rand::thread_rng();

	loop {
		let start: Instant = Instant::now();
		let status: ExitStatus = command.status().expect("failed to execute process");
		let elapsed: Duration = start.elapsed();

		iterations += 1;

		match status.code() {
			Some(0) => break,
			Some(_) => {}
			None => {
				eprintln!("Child terminated by signal; breaking");
				break;
			}
		}

		if let Some(SlotTime::AutoGenerated(dur)) = &slot_time {
			let dur: Duration = (*dur * (iterations - 1) + elapsed) / iterations;
			slot_time = Some(SlotTime::AutoGenerated(dur));
		} else if slot_time.is_none() {
			slot_time = Some(SlotTime::AutoGenerated(elapsed));
		}

		let delay: Duration = match &slot_time {
			Some(SlotTime::UserSpecified(dur)) | Some(SlotTime::AutoGenerated(dur)) => {
				let distribution = Uniform::new(0, 2_u32.pow(clamp(iterations + 1, 0, max_n)) - 1);
				let multiplicand: u32 = distribution.sample(&mut rng);

				*dur * multiplicand
			}
			None => Duration::new(0, 0),
		};

		sleep(delay);
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn clamp_i32_inside_range() {
		assert_eq!(clamp(1_i32, 0_i32, 2_i32), 1_i32);
	}

	#[test]
	fn clamp_i32_below_range() {
		assert_eq!(clamp(-1_i32, 0_i32, 2_i32), 0_i32);
	}

	#[test]
	fn clamp_i32_above_range() {
		assert_eq!(clamp(3_i32, 0_i32, 2_i32), 2_i32);
	}

	#[test]
	#[should_panic]
	fn clamp_panics_if_not_ordered_properly() {
		clamp(1_i32, 2_i32, 0_i32);
	}
}