mirror of
https://github.com/telemt/telemt.git
synced 2026-10-12 12:25:58 +03:00
Dashmap-driven Routing + Health Parallel + Family Runtime State
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use rand::RngExt;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::MeFloorMode;
|
||||
@@ -15,6 +16,7 @@ use crate::crypto::SecureRandom;
|
||||
use crate::network::IpFamily;
|
||||
|
||||
use super::MePool;
|
||||
use super::pool::MeFamilyRuntimeState;
|
||||
|
||||
const JITTER_FRAC_NUM: u64 = 2; // jitter up to 50% of backoff
|
||||
#[allow(dead_code)]
|
||||
@@ -28,6 +30,9 @@ const HEALTH_RECONNECT_BUDGET_PER_CORE: usize = 2;
|
||||
const HEALTH_RECONNECT_BUDGET_PER_DC: usize = 1;
|
||||
const HEALTH_RECONNECT_BUDGET_MIN: usize = 4;
|
||||
const HEALTH_RECONNECT_BUDGET_MAX: usize = 128;
|
||||
const FAMILY_SUPPRESS_FAIL_STREAK_THRESHOLD: u32 = 5;
|
||||
const FAMILY_SUPPRESS_DURATION_SECS: u64 = 60;
|
||||
const FAMILY_RECOVER_SUCCESS_STREAK_TARGET: u32 = 2;
|
||||
const HEALTH_DRAIN_CLOSE_BUDGET_PER_CORE: usize = 16;
|
||||
const HEALTH_DRAIN_CLOSE_BUDGET_MIN: usize = 16;
|
||||
const HEALTH_DRAIN_CLOSE_BUDGET_MAX: usize = 256;
|
||||
@@ -57,6 +62,17 @@ struct FamilyFloorPlan {
|
||||
target_writers_total: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FamilyReconnectOutcome {
|
||||
key: (i32, IpFamily),
|
||||
dc: i32,
|
||||
family: IpFamily,
|
||||
alive: usize,
|
||||
required: usize,
|
||||
endpoint_count: usize,
|
||||
restored: usize,
|
||||
}
|
||||
|
||||
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) {
|
||||
let mut backoff: HashMap<(i32, IpFamily), u64> = HashMap::new();
|
||||
let mut next_attempt: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||
@@ -115,6 +131,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
|
||||
&mut floor_warn_next_allowed,
|
||||
)
|
||||
.await;
|
||||
update_family_runtime_state(&pool, IpFamily::V4, v4_degraded);
|
||||
update_family_runtime_state(&pool, IpFamily::V6, v6_degraded);
|
||||
degraded_interval = v4_degraded || v6_degraded;
|
||||
}
|
||||
}
|
||||
@@ -430,6 +448,10 @@ async fn check_family(
|
||||
floor_plan.active_writers_current,
|
||||
floor_plan.warm_writers_current,
|
||||
);
|
||||
let live_writer_ids_by_addr = Arc::new(live_writer_ids_by_addr);
|
||||
let writer_idle_since = Arc::new(writer_idle_since);
|
||||
let bound_clients_by_writer = Arc::new(bound_clients_by_writer);
|
||||
let mut reconnect_set = JoinSet::<FamilyReconnectOutcome>::new();
|
||||
|
||||
for (dc, endpoints) in dc_endpoints {
|
||||
if endpoints.is_empty() {
|
||||
@@ -503,9 +525,9 @@ async fn check_family(
|
||||
&endpoints,
|
||||
alive,
|
||||
required,
|
||||
&live_writer_ids_by_addr,
|
||||
&writer_idle_since,
|
||||
&bound_clients_by_writer,
|
||||
live_writer_ids_by_addr.as_ref(),
|
||||
writer_idle_since.as_ref(),
|
||||
bound_clients_by_writer.as_ref(),
|
||||
idle_refresh_next_attempt,
|
||||
)
|
||||
.await;
|
||||
@@ -518,8 +540,8 @@ async fn check_family(
|
||||
&endpoints,
|
||||
alive,
|
||||
required,
|
||||
&live_writer_ids_by_addr,
|
||||
&bound_clients_by_writer,
|
||||
live_writer_ids_by_addr.as_ref(),
|
||||
bound_clients_by_writer.as_ref(),
|
||||
shadow_rotate_deadline,
|
||||
)
|
||||
.await;
|
||||
@@ -575,121 +597,165 @@ async fn check_family(
|
||||
continue;
|
||||
}
|
||||
*inflight.entry(key).or_insert(0) += 1;
|
||||
|
||||
let mut restored = 0usize;
|
||||
for _ in 0..missing {
|
||||
let Ok(reconnect_permit) = reconnect_sem.clone().try_acquire_owned() else {
|
||||
break;
|
||||
};
|
||||
if pool.active_contour_writer_count_total().await
|
||||
>= floor_plan.active_cap_effective_total
|
||||
{
|
||||
let swapped = maybe_swap_idle_writer_for_cap(
|
||||
pool,
|
||||
rng,
|
||||
dc,
|
||||
family,
|
||||
&endpoints,
|
||||
&live_writer_ids_by_addr,
|
||||
&writer_idle_since,
|
||||
&bound_clients_by_writer,
|
||||
let pool_for_reconnect = pool.clone();
|
||||
let rng_for_reconnect = rng.clone();
|
||||
let reconnect_sem_for_dc = reconnect_sem.clone();
|
||||
let endpoints_for_dc = endpoints.clone();
|
||||
let live_writer_ids_by_addr_for_dc = live_writer_ids_by_addr.clone();
|
||||
let writer_idle_since_for_dc = writer_idle_since.clone();
|
||||
let bound_clients_by_writer_for_dc = bound_clients_by_writer.clone();
|
||||
let active_cap_effective_total = floor_plan.active_cap_effective_total;
|
||||
reconnect_set.spawn(async move {
|
||||
let mut restored = 0usize;
|
||||
for _ in 0..missing {
|
||||
let Ok(reconnect_permit) = reconnect_sem_for_dc.clone().try_acquire_owned() else {
|
||||
break;
|
||||
};
|
||||
if pool_for_reconnect.active_contour_writer_count_total().await
|
||||
>= active_cap_effective_total
|
||||
{
|
||||
let swapped = maybe_swap_idle_writer_for_cap(
|
||||
&pool_for_reconnect,
|
||||
&rng_for_reconnect,
|
||||
dc,
|
||||
family,
|
||||
&endpoints_for_dc,
|
||||
live_writer_ids_by_addr_for_dc.as_ref(),
|
||||
writer_idle_since_for_dc.as_ref(),
|
||||
bound_clients_by_writer_for_dc.as_ref(),
|
||||
)
|
||||
.await;
|
||||
if swapped {
|
||||
pool_for_reconnect
|
||||
.stats
|
||||
.increment_me_floor_swap_idle_total();
|
||||
restored += 1;
|
||||
continue;
|
||||
}
|
||||
pool_for_reconnect
|
||||
.stats
|
||||
.increment_me_floor_cap_block_total();
|
||||
pool_for_reconnect
|
||||
.stats
|
||||
.increment_me_floor_swap_idle_failed_total();
|
||||
debug!(
|
||||
dc = %dc,
|
||||
?family,
|
||||
alive,
|
||||
required,
|
||||
active_cap_effective_total,
|
||||
"Adaptive floor cap reached, reconnect attempt blocked"
|
||||
);
|
||||
break;
|
||||
}
|
||||
let res = tokio::time::timeout(
|
||||
pool_for_reconnect.reconnect_runtime.me_one_timeout,
|
||||
pool_for_reconnect.connect_endpoints_round_robin(
|
||||
dc,
|
||||
&endpoints_for_dc,
|
||||
rng_for_reconnect.as_ref(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
if swapped {
|
||||
pool.stats.increment_me_floor_swap_idle_total();
|
||||
restored += 1;
|
||||
continue;
|
||||
match res {
|
||||
Ok(true) => {
|
||||
restored += 1;
|
||||
pool_for_reconnect.stats.increment_me_reconnect_success();
|
||||
}
|
||||
Ok(false) => {
|
||||
pool_for_reconnect.stats.increment_me_reconnect_attempt();
|
||||
debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
|
||||
}
|
||||
Err(_) => {
|
||||
pool_for_reconnect.stats.increment_me_reconnect_attempt();
|
||||
debug!(dc = %dc, ?family, "ME reconnect timed out");
|
||||
}
|
||||
}
|
||||
pool.stats.increment_me_floor_cap_block_total();
|
||||
pool.stats.increment_me_floor_swap_idle_failed_total();
|
||||
debug!(
|
||||
dc = %dc,
|
||||
?family,
|
||||
alive,
|
||||
required,
|
||||
active_cap_effective_total = floor_plan.active_cap_effective_total,
|
||||
"Adaptive floor cap reached, reconnect attempt blocked"
|
||||
);
|
||||
break;
|
||||
drop(reconnect_permit);
|
||||
}
|
||||
let res = tokio::time::timeout(
|
||||
pool.reconnect_runtime.me_one_timeout,
|
||||
pool.connect_endpoints_round_robin(dc, &endpoints, rng.as_ref()),
|
||||
)
|
||||
.await;
|
||||
match res {
|
||||
Ok(true) => {
|
||||
restored += 1;
|
||||
pool.stats.increment_me_reconnect_success();
|
||||
}
|
||||
Ok(false) => {
|
||||
pool.stats.increment_me_reconnect_attempt();
|
||||
debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
|
||||
}
|
||||
Err(_) => {
|
||||
pool.stats.increment_me_reconnect_attempt();
|
||||
debug!(dc = %dc, ?family, "ME reconnect timed out");
|
||||
}
|
||||
}
|
||||
drop(reconnect_permit);
|
||||
}
|
||||
|
||||
let now_alive = alive + restored;
|
||||
if now_alive >= required {
|
||||
info!(
|
||||
dc = %dc,
|
||||
?family,
|
||||
alive = now_alive,
|
||||
FamilyReconnectOutcome {
|
||||
key,
|
||||
dc,
|
||||
family,
|
||||
alive,
|
||||
required,
|
||||
endpoint_count = endpoints.len(),
|
||||
endpoint_count: endpoints_for_dc.len(),
|
||||
restored,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(joined) = reconnect_set.join_next().await {
|
||||
let outcome = match joined {
|
||||
Ok(outcome) => outcome,
|
||||
Err(join_error) => {
|
||||
debug!(error = %join_error, "Health reconnect task failed");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let now = Instant::now();
|
||||
let now_alive = outcome.alive + outcome.restored;
|
||||
if now_alive >= outcome.required {
|
||||
info!(
|
||||
dc = %outcome.dc,
|
||||
family = ?outcome.family,
|
||||
alive = now_alive,
|
||||
required = outcome.required,
|
||||
endpoint_count = outcome.endpoint_count,
|
||||
"ME writer floor restored for DC"
|
||||
);
|
||||
backoff.insert(
|
||||
key,
|
||||
outcome.key,
|
||||
pool.reconnect_runtime.me_reconnect_backoff_base.as_millis() as u64,
|
||||
);
|
||||
let jitter = pool.reconnect_runtime.me_reconnect_backoff_base.as_millis() as u64
|
||||
/ JITTER_FRAC_NUM;
|
||||
let wait = pool.reconnect_runtime.me_reconnect_backoff_base
|
||||
+ Duration::from_millis(rand::rng().random_range(0..=jitter.max(1)));
|
||||
next_attempt.insert(key, now + wait);
|
||||
next_attempt.insert(outcome.key, now + wait);
|
||||
} else {
|
||||
let curr = *backoff
|
||||
.get(&key)
|
||||
.get(&outcome.key)
|
||||
.unwrap_or(&(pool.reconnect_runtime.me_reconnect_backoff_base.as_millis() as u64));
|
||||
let next_ms = (curr.saturating_mul(2))
|
||||
.min(pool.reconnect_runtime.me_reconnect_backoff_cap.as_millis() as u64);
|
||||
backoff.insert(key, next_ms);
|
||||
backoff.insert(outcome.key, next_ms);
|
||||
let jitter = next_ms / JITTER_FRAC_NUM;
|
||||
let wait = Duration::from_millis(next_ms)
|
||||
+ Duration::from_millis(rand::rng().random_range(0..=jitter.max(1)));
|
||||
next_attempt.insert(key, now + wait);
|
||||
next_attempt.insert(outcome.key, now + wait);
|
||||
if pool.is_runtime_ready() {
|
||||
let warn_cooldown = pool.warn_rate_limit_duration();
|
||||
if should_emit_rate_limited_warn(floor_warn_next_allowed, key, now, warn_cooldown) {
|
||||
if should_emit_rate_limited_warn(
|
||||
floor_warn_next_allowed,
|
||||
outcome.key,
|
||||
now,
|
||||
warn_cooldown,
|
||||
) {
|
||||
warn!(
|
||||
dc = %dc,
|
||||
?family,
|
||||
dc = %outcome.dc,
|
||||
family = ?outcome.family,
|
||||
alive = now_alive,
|
||||
required,
|
||||
endpoint_count = endpoints.len(),
|
||||
required = outcome.required,
|
||||
endpoint_count = outcome.endpoint_count,
|
||||
backoff_ms = next_ms,
|
||||
"DC writer floor is below required level, scheduled reconnect"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
dc = %dc,
|
||||
?family,
|
||||
dc = %outcome.dc,
|
||||
family = ?outcome.family,
|
||||
alive = now_alive,
|
||||
required,
|
||||
endpoint_count = endpoints.len(),
|
||||
required = outcome.required,
|
||||
endpoint_count = outcome.endpoint_count,
|
||||
backoff_ms = next_ms,
|
||||
"DC writer floor is below required level during startup, scheduled reconnect"
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(v) = inflight.get_mut(&key) {
|
||||
if let Some(v) = inflight.get_mut(&outcome.key) {
|
||||
*v = v.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
@@ -706,6 +772,68 @@ fn health_reconnect_budget(pool: &Arc<MePool>, dc_groups: usize) -> usize {
|
||||
.clamp(HEALTH_RECONNECT_BUDGET_MIN, HEALTH_RECONNECT_BUDGET_MAX)
|
||||
}
|
||||
|
||||
fn update_family_runtime_state(pool: &Arc<MePool>, family: IpFamily, degraded: bool) {
|
||||
let now_epoch_secs = MePool::now_epoch_secs();
|
||||
let previous_state = pool.family_runtime_state(family);
|
||||
let mut state_since_epoch_secs = pool.family_runtime_state_since_epoch_secs(family);
|
||||
let previous_suppressed_until_epoch_secs = pool.family_suppressed_until_epoch_secs(family);
|
||||
let previous_fail_streak = pool.family_fail_streak(family);
|
||||
let previous_recover_success_streak = pool.family_recover_success_streak(family);
|
||||
|
||||
let (next_state, suppressed_until_epoch_secs, fail_streak, recover_success_streak) =
|
||||
if previous_suppressed_until_epoch_secs > now_epoch_secs {
|
||||
let fail_streak = if degraded {
|
||||
previous_fail_streak.saturating_add(1)
|
||||
} else {
|
||||
previous_fail_streak
|
||||
};
|
||||
(
|
||||
MeFamilyRuntimeState::Suppressed,
|
||||
previous_suppressed_until_epoch_secs,
|
||||
fail_streak,
|
||||
0,
|
||||
)
|
||||
} else if degraded {
|
||||
let fail_streak = previous_fail_streak.saturating_add(1);
|
||||
if fail_streak >= FAMILY_SUPPRESS_FAIL_STREAK_THRESHOLD {
|
||||
(
|
||||
MeFamilyRuntimeState::Suppressed,
|
||||
now_epoch_secs.saturating_add(FAMILY_SUPPRESS_DURATION_SECS),
|
||||
fail_streak,
|
||||
0,
|
||||
)
|
||||
} else {
|
||||
(MeFamilyRuntimeState::Degraded, 0, fail_streak, 0)
|
||||
}
|
||||
} else if matches!(previous_state, MeFamilyRuntimeState::Healthy) {
|
||||
(MeFamilyRuntimeState::Healthy, 0, 0, 0)
|
||||
} else {
|
||||
let recover_success_streak = previous_recover_success_streak.saturating_add(1);
|
||||
if recover_success_streak >= FAMILY_RECOVER_SUCCESS_STREAK_TARGET {
|
||||
(MeFamilyRuntimeState::Healthy, 0, 0, 0)
|
||||
} else {
|
||||
(
|
||||
MeFamilyRuntimeState::Recovering,
|
||||
0,
|
||||
0,
|
||||
recover_success_streak,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if next_state != previous_state || state_since_epoch_secs == 0 {
|
||||
state_since_epoch_secs = now_epoch_secs;
|
||||
}
|
||||
pool.set_family_runtime_state(
|
||||
family,
|
||||
next_state,
|
||||
state_since_epoch_secs,
|
||||
suppressed_until_epoch_secs,
|
||||
fail_streak,
|
||||
recover_success_streak,
|
||||
);
|
||||
}
|
||||
|
||||
fn should_emit_rate_limited_warn(
|
||||
next_allowed: &mut HashMap<(i32, IpFamily), Instant>,
|
||||
key: (i32, IpFamily),
|
||||
|
||||
Reference in New Issue
Block a user