Skip to content

Instantly share code, notes, and snippets.

@mryndzionek
Last active October 20, 2021 03:35
Show Gist options
  • Save mryndzionek/8641641f2e1cf877b867f186591cc20c to your computer and use it in GitHub Desktop.
Save mryndzionek/8641641f2e1cf877b867f186591cc20c to your computer and use it in GitHub Desktop.
Rust implementation of the 'Happy Eyeballs' (also called 'Fast Fallback') algorithm - https://en.wikipedia.org/wiki/Happy_Eyeballs
[package]
name = "happy_eyeballs"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = { version = "0.8" }
log = { version = "0.4" }
env_logger = { version = "0.9" }
chrono = { version = "0.4" }
ctrlc = { version = "3.1" }
[profile.release]
opt-level = 'z'
lto = true
debug = false
codegen-units = 1
panic = "abort"
use chrono::Local;
use log::LevelFilter;
use log::{info, warn};
use rand::Rng;
use std::io::Write;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::thread::JoinHandle;
use std::{thread, time::Duration};
const NUM_ACTIONS: usize = 5;
const LAST_ACTION: usize = NUM_ACTIONS - 1;
const ACTION_TIMEOUT_RANGE_MS: std::ops::Range<u64> = 2000..6000;
const WAIT_TIME_MS: u64 = 2000;
enum Event {
ActionFinished(usize),
KeyboardInterrupt,
}
fn action(id: usize, sch: Sender<Event>, rch: Receiver<()>) {
let delay_ms = rand::thread_rng().gen_range(ACTION_TIMEOUT_RANGE_MS);
info!("Action {:?} sleeping for {:?} ms", id, delay_ms);
match rch.recv_timeout(Duration::from_millis(delay_ms)) {
Ok(()) => {
warn!("Action {:?} canceled", id)
}
Err(_) => {
info!("Action {:?} finished", id);
sch.send(Event::ActionFinished(id)).unwrap();
}
}
}
fn spawn_new_action(
sch: Sender<Event>,
handles: &mut Vec<JoinHandle<()>>,
channels: &mut Vec<Sender<()>>,
id: usize,
) {
let (a_sch, a_rch) = mpsc::channel();
handles.push(thread::spawn(move || {
action(id, sch, a_rch);
}));
channels.push(a_sch);
}
fn ctrl_channel(sch: Sender<Event>) {
ctrlc::set_handler(move || {
let _ = sch.send(Event::KeyboardInterrupt);
})
.unwrap();
}
fn main() {
env_logger::Builder::new()
.format(|buf, record| {
writeln!(
buf,
"{} [{}] - {}",
Local::now().format("%H:%M:%S.%f"),
record.level(),
record.args()
)
})
.filter(None, LevelFilter::Info)
.init();
let (send, recv) = mpsc::channel();
let mut curr_id: usize = 0;
let mut handles = Vec::new();
let mut channels = Vec::new();
let mut actions_left = true;
ctrl_channel(send.clone());
// Spawn the first action
spawn_new_action(send.clone(), &mut handles, &mut channels, curr_id);
loop {
let r = if !actions_left {
// No new actions to spawn - wait indefinitely for some actions to finish
recv.recv().or(Err(RecvTimeoutError::Timeout))
} else {
// Wait with timeout if there are more actions to spawn
recv.recv_timeout(Duration::from_millis(WAIT_TIME_MS))
};
match r {
Ok(ev) => {
match ev {
Event::KeyboardInterrupt => {
warn!("Keyboard interrupt! Exiting...");
}
Event::ActionFinished(id) => {
info!("Action {:?} finished first", id);
info!("Canceling other actions");
channels.remove(id);
}
}
// Just send a 'cancel' message and wait for threads to finish
for ch in channels {
ch.send(()).unwrap();
}
for h in handles {
h.join().unwrap();
}
break;
}
Err(_) => {
warn!("Timeout!");
// Spawn new action if there are any left
if actions_left {
curr_id += 1;
info!("Spawning another action ({:?})", curr_id);
spawn_new_action(send.clone(), &mut handles, &mut channels, curr_id);
if curr_id == LAST_ACTION {
actions_left = false;
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment