mirror of
https://github.com/telemt/telemt.git
synced 2026-11-02 22:02:00 +03:00
ME: Autonomous&Lifecycle-bounded Pool Convergence
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user