blob: 9db8c35f0c05dc5bdb493286d79bd88efda8b7f8 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Synchronize the time steps of a simulation whose workers are grouped by region.
//!
//! This example models progress tracking for two regions with two workers each. Workers publish
//! their completed time step, then wait for their region's driver. Each driver represents its
//! region at a root phaser and releases local workers only after every region is ready. No region
//! can start the next step while another is still processing the current one.
//!
//! The first run completes three steps. The second fails a worker during step two and verifies
//! that all groups stop with only step one completed globally. The simulation's domain calculation
//! is omitted; the shared counters record each worker's completed step.
//!
//! The groups and drivers are explicit application code, with independent local phase counters
//! and fixed membership. This does not provide automatic parent registration or arbitrary
//! concurrent membership changes. Performance relative to a flat phaser depends on the workload.
use std::error::Error;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use asyncband::phaser::Closed;
use asyncband::phaser::Phaser;
use asyncband::phaser::PhaserParticipant;
const GROUPS: usize = 2;
const WORKERS_PER_GROUP: usize = 2;
const ROUNDS: u64 = 3;
struct AbortOnDrop {
phasers: [Phaser; 3],
armed: bool,
}
impl Drop for AbortOnDrop {
fn drop(&mut self) {
if self.armed {
for phaser in &self.phasers {
phaser.close();
}
}
}
}
struct LocalMember {
// Abort before dropping either participant, including when a task is never polled.
abort: AbortOnDrop,
ready: PhaserParticipant,
resume: PhaserParticipant,
}
impl LocalMember {
fn register(root: &Phaser, ready: &Phaser, resume: &Phaser) -> Result<Self, Closed> {
Ok(Self {
abort: AbortOnDrop {
phasers: [root.clone(), ready.clone(), resume.clone()],
armed: true,
},
ready: ready.register_one()?,
resume: resume.register_one()?,
})
}
}
async fn worker(
mut member: LocalMember,
id: usize,
values: Arc<Vec<AtomicU64>>,
fail: bool,
) -> Result<(), Box<dyn Error + Send + Sync>> {
for round in 1..=ROUNDS {
if fail && round == 2 {
// The abort guard closes the root before any participant is withdrawn.
return Err("input validation failed".into());
}
values[id].store(round, Ordering::Relaxed);
member.ready.wait().await?;
member.resume.wait().await?;
// Every group, not merely this worker's local group, must have published this round.
assert!(
values
.iter()
.all(|value| value.load(Ordering::Relaxed) >= round)
);
}
member.abort.armed = false;
Ok(())
}
struct GroupDriver {
// Keep the root obligation behind the local abort guard in the same owned task argument.
local: LocalMember,
root: PhaserParticipant,
}
async fn drive_group(mut driver: GroupDriver) -> Result<(), Box<dyn Error + Send + Sync>> {
for _ in 0..ROUNDS {
driver.local.ready.wait().await?;
driver.root.wait().await?;
driver.local.resume.wait().await?;
}
driver.local.abort.armed = false;
Ok(())
}
struct CloseRootOnDrop(Phaser);
impl Drop for CloseRootOnDrop {
fn drop(&mut self) {
self.0.close();
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
run_groups(false).await?;
assert!(run_groups(true).await.unwrap_err().is::<Closed>());
println!("group failure: every local group stopped after the root was closed");
Ok(())
}
async fn run_groups(fail_one_worker: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
let root = Phaser::new();
let mut coordinator = root.register_one()?;
// Created after the participant so cancellation closes the root before withdrawing it.
let _close_root = CloseRootOnDrop(root.clone());
let values = Arc::new(
(0..GROUPS * WORKERS_PER_GROUP)
.map(|_| AtomicU64::new(0))
.collect::<Vec<_>>(),
);
let mut tasks = Vec::new();
for group in 0..GROUPS {
let ready = Phaser::new();
let resume = Phaser::new();
let driver = LocalMember::register(&root, &ready, &resume)?;
let representative = root.register_one()?;
for worker_id in 0..WORKERS_PER_GROUP {
let member = LocalMember::register(&root, &ready, &resume)?;
let id = group * WORKERS_PER_GROUP + worker_id;
tasks.push(tokio::spawn(worker(
member,
id,
values.clone(),
fail_one_worker && id == 0,
)));
}
tasks.push(tokio::spawn(drive_group(GroupDriver {
local: driver,
root: representative,
})));
}
assert_eq!(root.registered_parties(), GROUPS + 1);
for round in 1..=ROUNDS {
if let Err(error) = coordinator.wait().await {
// After the root is closed, group drivers close their local phasers.
root.close();
let mut failures = 0;
for task in tasks {
let error = task.await.expect("group task panicked").unwrap_err();
if !error.is::<Closed>() {
assert_eq!(error.to_string(), "input validation failed");
failures += 1;
}
}
assert_eq!(failures, 1);
assert_eq!(root.phase(), 1);
return Err(error.into());
}
println!("root: all {GROUPS} groups completed round {round}");
}
for task in tasks {
task.await.expect("group task panicked")?;
}
assert!(
values
.iter()
.all(|value| value.load(Ordering::Relaxed) == ROUNDS)
);
println!("grouped fan-in: four workers synchronized through two root representatives");
Ok(())
}