ME: Autonomous&Lifecycle-bounded Pool Convergence

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-10-05 06:02:08 +03:00
parent 9c31e970e6
commit 556affc63a
37 changed files with 2546 additions and 685 deletions
@@ -0,0 +1,87 @@
use super::*;
use std::task::{Context, Poll};
/// Writer-local physical lifetime, independent of registry visibility.
pub(in crate::transport::middle_proxy) struct ReplacementHandoff {
state: parking_lot::Mutex<HandoffState>,
}
enum HandoffState {
Open(Option<WriterOpenReservation>),
Closed,
}
impl ReplacementHandoff {
/// Creates a handoff that may accept one physical-overlap token.
pub(in crate::transport::middle_proxy) fn new() -> Arc<Self> {
Arc::new(Self {
state: parking_lot::Mutex::new(HandoffState::Open(None)),
})
}
fn close(&self) {
*self.state.lock() = HandoffState::Closed;
}
/// Keeps attachment reversible until the final registry validation has succeeded.
pub(super) fn commit(
&self,
permit: &mut Option<WriterOpenReservation>,
validate: impl FnOnce() -> bool,
) -> bool {
let mut state = self.state.lock();
match &mut *state {
HandoffState::Open(slot) if slot.is_none() && permit.is_some() && validate() => {
*slot = permit.take();
true
}
_ => false,
}
}
}
/// Destroys transport futures before releasing either opening or retiring capacity.
pub(super) struct WriterTransport {
future: Option<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
/// Shared with the registry but closed only by transport destruction.
pub(super) lifetime: Arc<ReplacementHandoff>,
/// Moves to the victim on replacement or is released after normal publication.
pub(super) opening: Option<WriterOpenReservation>,
}
impl WriterTransport {
/// Owns prepared transports even if their task is never polled or published.
pub(super) fn new(
future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
opening: WriterOpenReservation,
) -> Self {
Self {
future: Some(future),
lifetime: ReplacementHandoff::new(),
opening: Some(opening),
}
}
}
impl Future for WriterTransport {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match self.future.as_mut() {
Some(future) => future.as_mut().poll(cx),
None => Poll::Ready(()),
}
}
}
impl Drop for WriterTransport {
fn drop(&mut self) {
// Explicit order also applies to unpolled futures, panic unwinding and abort.
drop(self.future.take());
self.lifetime.close();
drop(self.opening.take());
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,238 @@
use super::*;
use crate::transport::middle_proxy::pool_writer_security_tests::make_pool;
use std::sync::atomic::AtomicUsize;
struct DestructionProbe {
counter: Arc<AtomicUsize>,
destroyed: Arc<AtomicBool>,
}
impl Drop for DestructionProbe {
fn drop(&mut self) {
assert!(
self.counter.load(Ordering::Acquire) > 0,
"transport must be destroyed before its overlap token"
);
self.destroyed.store(true, Ordering::Release);
}
}
async fn transport(pool: &Arc<MePool>, destroyed: Arc<AtomicBool>) -> WriterTransport {
let permit = pool
.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
"127.0.0.1:443".parse().unwrap(),
)
.await
.unwrap();
let probe = DestructionProbe {
counter: pool.writer_replacement_open_reserved.clone(),
destroyed,
};
WriterTransport::new(
Box::pin(async move {
let _probe = probe;
std::future::pending::<()>().await;
}),
permit,
)
}
#[tokio::test]
async fn unpolled_transport_and_aborted_task_release_after_destruction() {
let pool = make_pool().await;
for abort in [false, true] {
let destroyed = Arc::new(AtomicBool::new(false));
let task = transport(&pool, destroyed.clone()).await;
if abort {
let task = tokio::spawn(task);
task.abort();
let _ = task.await;
} else {
drop(task);
}
assert!(destroyed.load(Ordering::Acquire));
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
}
}
#[tokio::test]
async fn retiring_transport_owns_capacity_after_logical_removal() {
let pool = make_pool().await;
let destroyed = Arc::new(AtomicBool::new(false));
let mut victim = transport(&pool, destroyed.clone()).await;
drop(victim.opening.take());
let lifetime = victim.lifetime.clone();
let mut successor = transport(&pool, Arc::new(AtomicBool::new(false))).await;
successor.future = Some(Box::pin(std::future::pending()));
assert!(lifetime.commit(&mut successor.opening, || true));
assert!(successor.opening.is_none());
// Registry removal only drops its handle; the real transport retains the token.
drop(lifetime);
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
1
);
drop(victim);
assert!(destroyed.load(Ordering::Acquire));
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
// Successor is no longer overlap after its victim has physically gone.
drop(successor);
}
#[tokio::test]
async fn failed_or_closed_handoff_keeps_the_prepared_token() {
let pool = make_pool().await;
let mut prepared = transport(&pool, Arc::new(AtomicBool::new(false))).await;
let lifetime = ReplacementHandoff::new();
assert!(!lifetime.commit(&mut prepared.opening, || false));
assert!(prepared.opening.is_some());
lifetime.close();
assert!(!lifetime.commit(&mut prepared.opening, || true));
assert!(prepared.opening.is_some());
drop(prepared);
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
}
#[tokio::test]
async fn overlap_limit_survives_commit_and_cap_shrink_until_teardown() {
let pool = make_pool().await;
pool.floor_runtime
.me_adaptive_floor_cpu_cores_override
.store(1, Ordering::Relaxed);
pool.floor_runtime
.me_adaptive_floor_max_active_writers_per_core
.store(1, Ordering::Relaxed);
pool.floor_runtime
.me_adaptive_floor_max_active_writers_global
.store(1, Ordering::Relaxed);
pool.set_adaptive_floor_runtime_caps(pool.floor_authority(), 1, 2, 1, 1, 1, 0, 0);
let mut victim = transport(&pool, Arc::new(AtomicBool::new(false))).await;
drop(victim.opening.take());
let mut successor = transport(&pool, Arc::new(AtomicBool::new(false))).await;
successor.future = Some(Box::pin(std::future::pending()));
assert!(victim.lifetime.commit(&mut successor.opening, || true));
let second = transport(&pool, Arc::new(AtomicBool::new(false))).await;
pool.set_adaptive_floor_runtime_caps(pool.floor_authority(), 1, 1, 1, 1, 1, 1, 0);
let addr = "127.0.0.1:443".parse().unwrap();
assert!(
pool.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
addr
)
.await
.is_none()
);
drop(second);
assert!(
pool.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
addr
)
.await
.is_none()
);
drop(victim);
assert!(
pool.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
addr
)
.await
.is_some()
);
drop(successor);
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
}
#[tokio::test]
async fn panicking_transport_releases_capacity_after_unwinding_its_resources() {
let pool = make_pool().await;
let permit = pool
.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
"127.0.0.1:443".parse().unwrap(),
)
.await
.unwrap();
let destroyed = Arc::new(AtomicBool::new(false));
let probe = DestructionProbe {
counter: pool.writer_replacement_open_reserved.clone(),
destroyed: destroyed.clone(),
};
let task = WriterTransport::new(
Box::pin(async move {
let _probe = probe;
panic!("injected transport panic");
}),
permit,
);
assert!(tokio::spawn(task).await.unwrap_err().is_panic());
assert!(destroyed.load(Ordering::Acquire));
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn racing_commit_and_close_release_each_token_exactly_once() {
let pool = make_pool().await;
for _ in 0..128 {
let permit = pool
.reserve_writer_open(
WriterContour::Active,
WriterOpenIntent::Replacement,
2,
"127.0.0.1:443".parse().unwrap(),
)
.await
.unwrap();
let lifetime = ReplacementHandoff::new();
let closing = lifetime.clone();
let close = tokio::spawn(async move {
tokio::task::yield_now().await;
closing.close();
});
let commit = tokio::spawn(async move {
let mut permit = Some(permit);
lifetime.commit(&mut permit, || true);
drop(permit);
});
close.await.unwrap();
commit.await.unwrap();
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
0
);
}
}
@@ -11,9 +11,8 @@ impl MePool {
tx,
byte_budget,
task_registration,
writer_task,
mut writer_task,
intent,
_open_reservation,
} = prepared;
let writer_id = writer.id;
let mut writers = self.writers.write().await;
@@ -22,9 +21,15 @@ impl MePool {
let contour = self.authorize_writer_publication(&writer, &coordinator)?;
self.authorize_writer_publication_capacity(&writer, contour, intent, writers.as_slice())?;
writer.contour.store(contour.as_u8(), Ordering::Release);
registry_registration.install(writer_id, tx, byte_budget);
registry_registration.install_with_lifetime(
writer_id,
tx,
byte_budget,
writer_task.lifetime.clone(),
);
writers.push(writer);
self.conn_count.fetch_add(1, Ordering::Relaxed);
drop(writer_task.opening.take());
self.lifecycle
.spawn_registered_writer(task_registration, writer_task);
drop(coordinator);
@@ -36,7 +36,7 @@ fn prepared<'a>(
pool: &'a Arc<MePool>,
writer: MeWriter,
intent: WriterOpenIntent,
reservation: WriterOpenReservation<'a>,
reservation: WriterOpenReservation,
task_started: Arc<AtomicBool>,
) -> PreparedWriter<'a> {
let cancel = writer.cancel.clone();
@@ -45,12 +45,14 @@ fn prepared<'a>(
byte_budget: writer.byte_budget.clone(),
writer,
task_registration: pool.lifecycle.try_register().unwrap(),
writer_task: Box::pin(async move {
task_started.store(true, Ordering::Release);
cancel.cancelled().await;
}),
writer_task: WriterTransport::new(
Box::pin(async move {
task_started.store(true, Ordering::Release);
cancel.cancelled().await;
}),
reservation,
),
intent,
_open_reservation: reservation,
}
}
@@ -44,9 +44,8 @@ impl MePool {
tx,
byte_budget,
task_registration,
writer_task,
intent,
_open_reservation,
mut writer_task,
intent: _,
} = prepared;
let replacement_writer_id = writer.id;
let victim_writer_id = reservation.writer_id();
@@ -57,7 +56,25 @@ impl MePool {
let mut registry_registration = self.registry.prepare_writer_registration().await;
let coordinator = self.reinit.coordinator.lock();
let contour = self.authorize_writer_publication(&writer, &coordinator)?;
self.authorize_writer_publication_capacity(&writer, contour, intent, writers.as_slice())?;
// Only this victim-backed transaction may replace Active coverage during suppression.
let family = if writer.addr.is_ipv4() {
crate::network::IpFamily::V4
} else {
crate::network::IpFamily::V6
};
let configured = if writer.addr.is_ipv4() {
self.decision.ipv4_me
} else {
self.decision.ipv6_me
};
if !configured
|| (contour != WriterContour::Active
&& self.is_family_temporarily_suppressed(family, Self::now_epoch_secs()))
{
return Err(ProxyError::Proxy(
"ME replacement family lost publication authority".into(),
));
}
writer.contour.store(contour.as_u8(), Ordering::Release);
let Some(victim_pos) = writers
.iter()
@@ -73,6 +90,17 @@ impl MePool {
"ME replacement victim changed role before commit".into(),
));
}
if !matches!(
purpose,
WriterReplacementPurpose::FloorRebalance { .. }
| WriterReplacementPurpose::CoverageTransfer { .. }
) && (expected_victim_role.dc != writer.writer_dc
|| expected_victim_role.family != family)
{
return Err(ProxyError::Proxy(
"ME replacement purpose cannot move coverage between groups".into(),
));
}
if let WriterReplacementPurpose::FloorRebalance {
donor_floor,
receiver_floor,
@@ -122,19 +150,103 @@ impl MePool {
}) == receiver_family
})
.count();
if donor_count <= donor_floor || receiver_count >= receiver_floor {
let protected_floor = self.required_writers_for_dc_with_floor_mode(
endpoint_snapshot
.endpoints_for_dc_family(expected_victim_role.dc, expected_victim_role.family)
.len(),
false,
);
let current_receiver_floor = self.required_writers_for_dc_with_floor_mode(
endpoint_snapshot
.endpoints_for_dc_family(writer.writer_dc, receiver_family)
.len(),
false,
);
if donor_count <= donor_floor.max(protected_floor)
|| receiver_count >= receiver_floor.min(current_receiver_floor)
{
return Err(ProxyError::Proxy(
"ME floor rebalance became unnecessary before commit".into(),
));
}
}
if !registry_registration.prepare_replacement_commit(reservation) {
if let WriterReplacementPurpose::CoverageTransfer {
authority,
receiver_floor,
} = purpose
{
if authority
!= (
coordinator.endpoint_revision,
coordinator.floor_policy_revision,
)
|| contour != WriterContour::Active
|| expected_victim_role.contour != WriterContour::Active
{
return Err(ProxyError::Proxy(
"ME coverage transfer lost policy authority".into(),
));
}
let endpoints = self.endpoint_snapshot.load();
let count = |dc, family| {
writers
.iter()
.filter(|candidate| {
!candidate.draining.load(Ordering::Acquire)
&& candidate.generation == coordinator.active_generation
&& candidate.writer_dc == dc
&& WriterContour::from_u8(candidate.contour.load(Ordering::Acquire))
== WriterContour::Active
&& candidate.addr.is_ipv4()
== matches!(family, crate::network::IpFamily::V4)
&& endpoints.contains_dc_endpoint(dc, candidate.addr)
})
.count()
};
let donor_is_current = victim.generation == coordinator.active_generation
&& endpoints.contains_dc_endpoint(victim.writer_dc, victim.addr);
let donor_floor = self.required_writers_for_dc_with_floor_mode(
endpoints
.endpoints_for_dc_family(victim.writer_dc, expected_victim_role.family)
.len(),
false,
);
if (donor_is_current
&& count(victim.writer_dc, expected_victim_role.family) <= donor_floor)
|| count(writer.writer_dc, family)
>= receiver_floor.min(
self.required_writers_for_dc_with_floor_mode(
endpoints
.endpoints_for_dc_family(writer.writer_dc, family)
.len(),
false,
),
)
{
return Err(ProxyError::Proxy(
"ME coverage transfer no longer preserves floors".into(),
));
}
}
let lifetime = registry_registration
.writer_lifetime(victim_writer_id)
.ok_or_else(|| {
ProxyError::Proxy("ME replacement victim transport disappeared".into())
})?;
if !lifetime.commit(&mut writer_task.opening, || {
registry_registration.prepare_replacement_commit(reservation)
}) {
return Err(ProxyError::Proxy(
"ME replacement victim became active before commit".into(),
));
}
registry_registration.install(replacement_writer_id, tx, byte_budget);
registry_registration.install_with_lifetime(
replacement_writer_id,
tx,
byte_budget,
writer_task.lifetime.clone(),
);
writers.push(writer);
self.conn_count.fetch_add(1, Ordering::Relaxed);
writers.publish_current();
@@ -158,6 +270,7 @@ impl MePool {
#[cfg(test)]
mod tests {
mod convergence;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
@@ -248,14 +361,54 @@ mod tests {
tx,
byte_budget,
task_registration,
writer_task: Box::pin(async move {
drop(rx);
}),
writer_task: WriterTransport::new(
Box::pin(async move {
drop(rx);
}),
open_reservation,
),
intent: WriterOpenIntent::Replacement,
_open_reservation: open_reservation,
}
}
#[tokio::test]
async fn suppressed_victim_backed_replacement_keeps_coverage() {
let pool = make_pool().await;
let addr = endpoint(8);
pool.update_proxy_maps(HashMap::from([(2, vec![(addr.ip(), addr.port())])]), None)
.await;
let victim = install_writer(&pool, 8001, 2, addr).await;
pool.set_family_runtime_state(
crate::network::IpFamily::V4,
crate::transport::middle_proxy::pool::MeFamilyRuntimeState::Suppressed,
MePool::now_epoch_secs(),
MePool::now_epoch_secs() + 3600,
5,
0,
);
let mut reservation = pool
.registry
.try_reserve_writer_replacement(victim.id)
.await
.unwrap();
let prepared = prepared_writer(&pool, 8002, 2, addr).await;
pool.publish_prepared_replacement_writer(
prepared,
WriterRole::from_writer(&victim),
WriterReplacementPurpose::IdleRefresh,
&mut reservation,
)
.await
.expect("a reserved active victim must remain replaceable during suppression");
assert!(victim.draining.load(Ordering::Acquire));
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
1,
"overlap is owned until the victim transport is destroyed"
);
}
#[tokio::test]
async fn replacement_commit_publishes_successor_before_draining_victim() {
let pool = make_pool().await;
@@ -0,0 +1,198 @@
use super::*;
use crate::transport::middle_proxy::registry::ConnMeta;
#[tokio::test]
async fn busy_obsolete_donor_preserves_clients_and_rechecks_policy() {
for stale in [false, true] {
let pool = make_pool().await;
let obsolete = endpoint(21);
let receiver = endpoint(22);
pool.update_proxy_maps(
HashMap::from([(2, vec![(receiver.ip(), receiver.port())])]),
None,
)
.await;
let victim = install_writer(&pool, 9001, 1, obsolete).await;
let (client, _messages) = pool.registry.register().await;
assert!(
pool.registry
.bind_writer(
client,
victim.id,
ConnMeta {
target_dc: 1,
client_addr: obsolete,
our_addr: obsolete,
proto_flags: 0,
}
)
.await
);
let authority = pool.floor_authority();
let mut reservation = pool
.registry
.try_reserve_writer_replacement_preserving_clients(victim.id)
.await
.unwrap();
let prepared = prepared_writer(&pool, 9002, 2, receiver).await;
if stale {
pool.reinit.coordinator.lock().floor_policy_revision += 1;
}
let result = pool
.publish_prepared_replacement_writer(
prepared,
WriterRole::from_writer(&victim),
WriterReplacementPurpose::CoverageTransfer {
authority,
receiver_floor: pool.required_writers_for_dc(1),
},
&mut reservation,
)
.await;
assert_eq!(result.is_ok(), !stale);
assert_eq!(victim.draining.load(Ordering::Acquire), !stale);
assert_eq!(
pool.registry.get_writer(client).await.unwrap().writer_id,
victim.id
);
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
usize::from(!stale)
);
}
}
#[tokio::test]
async fn transfer_commit_matches_independent_donor_floor_model() {
// The model counts only current endpoints; obsolete sockets contribute no coverage.
for obsolete in [false, true] {
for donor_delta in [0usize, 1] {
for receiver_full in [false, true] {
let pool = make_pool().await;
let donor = endpoint(31);
let receiver = endpoint(32);
pool.update_proxy_maps(
HashMap::from([
(1, vec![(donor.ip(), donor.port())]),
(2, vec![(receiver.ip(), receiver.port())]),
]),
None,
)
.await;
let floor = pool.required_writers_for_dc(1);
let mut victim = None;
for id in 0..(floor + donor_delta) {
let writer = install_writer(&pool, 10000 + id as u64, 1, donor).await;
if id == 0 {
victim = Some(writer);
}
}
let mut victim = victim.unwrap();
if obsolete {
let mut writers = pool.writers.write().await;
writers
.iter_mut()
.find(|writer| writer.id == victim.id)
.unwrap()
.addr = endpoint(33);
victim.addr = endpoint(33);
}
if receiver_full {
for id in 0..floor {
install_writer(&pool, 11000 + id as u64, 2, receiver).await;
}
}
let mut reservation = pool
.registry
.try_reserve_writer_replacement(victim.id)
.await
.unwrap();
let prepared = prepared_writer(&pool, 12000, 2, receiver).await;
let result = pool
.publish_prepared_replacement_writer(
prepared,
WriterRole::from_writer(&victim),
WriterReplacementPurpose::CoverageTransfer {
authority: pool.floor_authority(),
receiver_floor: floor,
},
&mut reservation,
)
.await;
assert_eq!(
result.is_ok(),
!receiver_full && (obsolete || donor_delta > 0),
"obsolete={obsolete}, surplus={donor_delta}, receiver_full={receiver_full}"
);
}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_transfers_cannot_overfill_receiver_or_steal_donor_floor() {
let pool = make_pool().await;
let donor = endpoint(41);
let receiver = endpoint(42);
pool.update_proxy_maps(
HashMap::from([
(1, vec![(donor.ip(), donor.port())]),
(2, vec![(receiver.ip(), receiver.port())]),
]),
None,
)
.await;
pool.set_adaptive_floor_runtime_caps(pool.floor_authority(), 128, 128, 128, 128, 6, 0, 0);
let floor = pool.required_writers_for_dc(1);
let count = floor + 8;
let mut victims = Vec::new();
for id in 0..count {
victims.push(install_writer(&pool, 20000 + id as u64, 1, donor).await);
}
let mut tasks = tokio::task::JoinSet::new();
for (index, victim) in victims.into_iter().enumerate() {
let pool = pool.clone();
tasks.spawn(async move {
let authority = pool.floor_authority();
let mut reservation = pool
.registry
.try_reserve_writer_replacement(victim.id)
.await
.unwrap();
let prepared = prepared_writer(&pool, 21000 + index as u64, 2, receiver).await;
tokio::task::yield_now().await;
pool.publish_prepared_replacement_writer(
prepared,
WriterRole::from_writer(&victim),
WriterReplacementPurpose::CoverageTransfer {
authority,
receiver_floor: floor,
},
&mut reservation,
)
.await
.is_ok()
});
}
let mut committed = 0;
while let Some(result) = tasks.join_next().await {
committed += usize::from(result.unwrap());
}
assert_eq!(committed, floor);
let writers = pool.writers.read().await;
let live = |dc| {
writers
.iter()
.filter(|writer| writer.writer_dc == dc && !writer.draining.load(Ordering::Acquire))
.count()
};
assert_eq!(live(2), floor);
assert_eq!(live(1), count - floor);
assert!(live(1) >= floor);
assert_eq!(
pool.writer_replacement_open_reserved
.load(Ordering::Acquire),
committed
);
}
@@ -291,9 +291,8 @@ impl MePool {
tx,
byte_budget,
task_registration,
writer_task,
writer_task: WriterTransport::new(writer_task, writer_open_reservation),
intent,
_open_reservation: writer_open_reservation,
})
}