blob: cef5d205d7a9e47497533993ef6bd69732b1efe4 [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.
//! Publish consistent progress snapshots from parallel import workers.
//!
//! Each worker contributes its cumulative record count after a batch. The coordinator sums those
//! counts and publishes a snapshot before workers process the next batch. Separate ready/resume
//! phasers prevent a fast worker from updating its count while the snapshot is being prepared.
//! The coordinator can also await an asynchronous checkpoint before releasing the workers.
//!
//! The first scenario stops once the total reaches a target. The other two show how a failed or
//! cancelled worker stops its peers, including a task cancelled before its first poll. Import work
//! is represented by counters, and checkpoint I/O by a yield.
//!
//! Membership is fixed within this protocol; changes must update both groups at a common batch
//! boundary. Each phaser has its own counter, distinct from the application's batch number.
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;
// Own this guard before constructing a task future so cancelling an unpolled task also aborts.
struct CloseOnDrop([Phaser; 2]);
impl Drop for CloseOnDrop {
fn drop(&mut self) {
for phaser in &self.0 {
phaser.close();
}
}
}
struct Member {
// Fields drop in declaration order: close before withdrawing any arrival obligation.
_close: CloseOnDrop,
ready: PhaserParticipant,
resume: PhaserParticipant,
}
impl Member {
fn register(ready: &Phaser, resume: &Phaser) -> Result<Self, Closed> {
Ok(Self {
_close: CloseOnDrop([ready.clone(), resume.clone()]),
ready: ready.register_one()?,
resume: resume.register_one()?,
})
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Closed> {
publish_until_target().await?;
failure_closes_the_group().await?;
cancelling_an_unpolled_task_closes_the_group().await?;
Ok(())
}
async fn compute(
mut member: Member,
id: usize,
values: Arc<Vec<AtomicU64>>,
published: Arc<AtomicU64>,
) -> Result<(), Closed> {
for round in 1_u64.. {
// The resume rendezvous must publish the previous aggregate before this read.
assert_eq!(published.load(Ordering::Relaxed), (round - 1) * 6);
values[id].store((id as u64 + 1) * round, Ordering::Relaxed);
member.ready.wait().await?;
member.resume.wait().await?;
}
unreachable!()
}
async fn publish_until_target() -> Result<(), Closed> {
let ready = Phaser::new();
let resume = Phaser::new();
let mut coordinator = Member::register(&ready, &resume)?;
let values = Arc::new((0..3).map(|_| AtomicU64::new(0)).collect::<Vec<_>>());
let published = Arc::new(AtomicU64::new(0));
let mut tasks = Vec::new();
// Register everyone before polling any worker; the coordinator also keeps both phases open.
for id in 0..3 {
tasks.push(tokio::spawn(compute(
Member::register(&ready, &resume)?,
id,
values.clone(),
published.clone(),
)));
}
loop {
coordinator.ready.wait().await?;
let sum: u64 = values
.iter()
.map(|value| value.load(Ordering::Relaxed))
.sum();
// An async checkpoint can be awaited here while workers wait at resume.
tokio::task::yield_now().await;
published.store(sum, Ordering::Relaxed);
println!("coordinator: published aggregate {sum}");
if sum >= 18 {
// Stop without releasing anyone into another computation round.
ready.close();
resume.close();
break;
}
coordinator.resume.wait().await?;
}
for task in tasks {
assert!(task.await.expect("worker panicked").is_err());
}
assert_eq!(published.load(Ordering::Relaxed), 18);
println!("target reached: all workers stopped after 18 imported records");
Ok(())
}
async fn wait_once(mut member: Member) -> Result<(), Closed> {
member.ready.wait().await?;
member.resume.wait().await?;
Ok(())
}
async fn fail(_member: Member) -> Result<(), &'static str> {
// The job error stays in its result; Closed tells peers that no next round is available.
Err("input validation failed")
}
async fn failure_closes_the_group() -> Result<(), Closed> {
let ready = Phaser::new();
let resume = Phaser::new();
let healthy = Member::register(&ready, &resume)?;
let failing = Member::register(&ready, &resume)?;
let (peer, failure) = tokio::join!(wait_once(healthy), fail(failing));
assert!(peer.is_err());
assert_eq!(failure, Err("input validation failed"));
assert_eq!(ready.phase(), 0);
assert_eq!(resume.phase(), 0);
println!("failure: peers observed Closed, not successful phase completion");
Ok(())
}
async fn cancelling_an_unpolled_task_closes_the_group() -> Result<(), Closed> {
let ready = Phaser::new();
let resume = Phaser::new();
let peer = Member::register(&ready, &resume)?;
let cancelled = wait_once(Member::register(&ready, &resume)?);
drop(cancelled);
assert!(wait_once(peer).await.is_err());
assert_eq!(ready.phase(), 0);
println!("cancellation: dropping an unpolled task closed both gates");
Ok(())
}