use clusterflux::{Artifact, EnvRef, SourceSnapshot}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, clusterflux::TaskArg)] pub struct BuildReport { pub linux_thread: u64, pub linux_parallel_thread: u64, pub package_thread: u64, pub linux_artifact: Artifact, pub package_artifact: Artifact, pub source: SourceSnapshot, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, clusterflux::TaskArg)] pub struct PackageInput { pub release_name: String, pub source: SourceSnapshot, pub executable: Option, pub inputs: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, clusterflux::TaskArg)] pub struct IdentityProbeInput { pub source: SourceSnapshot, pub label: String, pub delay_seconds: u64, } pub fn linux_env() -> EnvRef { clusterflux::env!("linux") } pub fn windows_env() -> EnvRef { clusterflux::env!("windows") } #[clusterflux::task(capabilities = "source_filesystem")] pub async fn prepare_source() -> SourceSnapshot { #[cfg(target_arch = "wasm32")] { return clusterflux::source::snapshot() .await .expect("the source task should snapshot the node checkout it was placed on"); } #[cfg(not(target_arch = "wasm32"))] SourceSnapshot { digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" .to_owned(), } } #[clusterflux::task(capabilities = "command")] pub async fn compile_linux(source: SourceSnapshot) -> Artifact { let _ = &source; #[cfg(target_arch = "wasm32")] { let executable = clusterflux::fs::output_path("hello-clusterflux") .expect("the executable output path should be task-local"); let output = clusterflux::command::Command::new("cc") .args([ "-Os", "-static", "-s", "fixture/hello-clusterflux.c", "-o", executable.as_str(), ]) .current_dir("/workspace") .env("SOURCE_DATE_EPOCH", "0") .timeout(std::time::Duration::from_secs(180)) .network_disabled() .output() .await .expect("the declared Linux environment should provide a real C compiler"); assert_eq!(output.status_code, Some(0)); return clusterflux::fs::flush(&executable) .await .expect("the command-created executable should flush to node artifact storage"); } #[cfg(not(target_arch = "wasm32"))] Artifact { id: "hello-clusterflux-native-test".to_owned(), digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" .to_owned(), size_bytes: 0, } } #[clusterflux::task] #[unsafe(no_mangle)] pub extern "C" fn task_add_one(input: i32) -> i32 { input + 1 } #[clusterflux::task] #[unsafe(no_mangle)] pub extern "C" fn task_trap(_input: i32) -> i32 { #[cfg(target_arch = "wasm32")] core::arch::wasm32::unreachable(); #[cfg(not(target_arch = "wasm32"))] panic!("intentional task trap") } #[clusterflux::task(capabilities = "command")] pub async fn package_release(input: PackageInput) -> Artifact { #[cfg(target_arch = "wasm32")] { assert_eq!(input.release_name, "release.tar"); let executable = input .executable .as_ref() .expect("the package input should contain the compiler artifact"); assert!(input.inputs.iter().any(|artifact| artifact == executable)); let executable = clusterflux::fs::materialize(&executable, "package/hello-clusterflux") .await .expect("the retaining node should materialize the compiler artifact locally"); let release = clusterflux::fs::output_path("release.tar") .expect("the release output path should be task-local"); let output = clusterflux::command::Command::new("tar") .args([ "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", "--mode=0755", "-cf", release.as_str(), "-C", "/clusterflux/output/package", "hello-clusterflux", ]) .current_dir("/workspace") .env("SOURCE_DATE_EPOCH", "0") .timeout(std::time::Duration::from_secs(180)) .network_disabled() .output() .await .expect("the declared Linux environment should package the materialized executable"); assert_eq!(output.status_code, Some(0)); assert_eq!( executable.as_str(), "/clusterflux/output/package/hello-clusterflux" ); return clusterflux::fs::flush(&release) .await .expect("the command-created release archive should flush as the final artifact"); } #[cfg(not(target_arch = "wasm32"))] { let _ = input; Artifact { id: "release-native-test".to_owned(), digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" .to_owned(), size_bytes: 0, } } } #[clusterflux::task(capabilities = "command")] pub async fn abort_probe(source: SourceSnapshot) -> i32 { let _ = &source; #[cfg(target_arch = "wasm32")] { let output = clusterflux::command::Command::new("sh") .args(["-c", "sleep 90"]) .timeout(std::time::Duration::from_secs(120)) .network_disabled() .output() .await .expect("abort probe command should either finish or be stopped by process abort"); return output.status_code.unwrap_or(-1); } #[cfg(not(target_arch = "wasm32"))] 0 } #[clusterflux::task(capabilities = "command")] pub async fn long_join_probe(source: SourceSnapshot) -> i32 { let _ = &source; #[cfg(target_arch = "wasm32")] { let output = clusterflux::command::Command::new("sh") .args(["-c", "sleep 125"]) .timeout(std::time::Duration::from_secs(180)) .network_disabled() .output() .await .expect("the controlled long-running command should complete normally"); return output.status_code.unwrap_or(-1); } #[cfg(not(target_arch = "wasm32"))] 0 } #[clusterflux::task(capabilities = "command")] pub async fn identity_probe(input: IdentityProbeInput) -> String { let _ = &input.source; #[cfg(target_arch = "wasm32")] { let script = format!( "sleep {}; printf '%s' '{}'", input.delay_seconds, input.label ); let output = clusterflux::command::Command::new("sh") .args(["-c", script.as_str()]) .timeout(std::time::Duration::from_secs(60)) .network_disabled() .output() .await .expect("identity probe command should complete"); assert_eq!(output.status_code, Some(0)); return input.label; } #[cfg(not(target_arch = "wasm32"))] input.label } #[clusterflux::task] pub fn cooperative_cancellation_probe() -> i32 { #[cfg(target_arch = "wasm32")] loop { if clusterflux::process::cancellation_requested() .expect("task control should remain available while the Wasm task is running") { // Cancellation is a request observed and handled by task code. Returning // normally is intentionally distinct from the runtime's forced abort path. return 17; } } #[cfg(not(target_arch = "wasm32"))] 17 } #[clusterflux::task] pub fn debug_child_probe() -> i32 { #[cfg(target_arch = "wasm32")] loop { if clusterflux::process::cancellation_requested() .expect("child debug participant should retain task control") { return 23; } } #[cfg(not(target_arch = "wasm32"))] 23 } #[clusterflux::task] pub async fn debug_parent_probe() -> i32 { #[cfg(target_arch = "wasm32")] { let child = clusterflux::spawn::task(debug_child_probe) .name("debug child participant") .start() .await .expect("parent debug participant should spawn its child"); return child .join() .await .expect("parent debug participant should join its child"); } #[cfg(not(target_arch = "wasm32"))] 23 } #[clusterflux::main] pub async fn build_main() -> BuildReport { run_build_workflow().await } #[clusterflux::main(name = "fail")] pub async fn fail_main() -> Result { #[cfg(target_arch = "wasm32")] { let child = clusterflux::spawn::task_with_arg(0_i32, |value| task_trap(value)) .task_id("task_trap") .name("intentional failing child") .start() .await .map_err(|error| format!("failing entrypoint could not launch its child: {error}"))?; return child .join() .await .map_err(|error| format!("intentional child failure: {error}")); } #[cfg(not(target_arch = "wasm32"))] Err("intentional child failure".to_owned()) } #[clusterflux::main(name = "restart")] pub async fn restart_main() -> i32 { #[cfg(target_arch = "wasm32")] { return clusterflux::spawn::task_with_arg(0_i32, |value| task_trap(value)) .task_id("task_trap") .name("edited restart probe") .failure_policy(clusterflux::spawn::TaskFailurePolicy::AwaitOperator) .start() .await .expect("restart probe should launch its child") .join() .await .expect("restart probe child should complete"); } #[cfg(not(target_arch = "wasm32"))] task_add_one(41) } #[clusterflux::main(name = "park-wake")] pub async fn park_wake_main() -> i32 { #[cfg(target_arch = "wasm32")] { let mut value = 0_i32; for _ in 0..16 { let next = clusterflux::spawn::task_with_arg(value, |input| task_add_one(input)) .task_id("task_add_one") .name("park/wake fuel refill probe") .start() .await .expect("park/wake probe should launch") .join() .await .expect("park/wake probe should complete"); value = i32::try_from(next).expect("park/wake result should remain in i32 range"); } return value; } #[cfg(not(target_arch = "wasm32"))] 16 } #[clusterflux::main(name = "long-join")] pub async fn long_join_main() -> i32 { #[cfg(target_arch = "wasm32")] { let source = clusterflux::spawn::async_task(prepare_source) .name("prepare source for controlled long-running task") .start() .await .expect("long join source preparation should launch") .join() .await .expect("long join source preparation should complete"); return clusterflux::spawn::async_task_with_arg(source, long_join_probe) .name("controlled task longer than two minutes") .env(linux_env()) .start() .await .expect("long join probe should launch") .join() .await .expect("long join probe should remain joinable beyond two minutes"); } #[cfg(not(target_arch = "wasm32"))] 0 } #[clusterflux::main(name = "identity")] pub async fn identity_main() -> Vec { #[cfg(target_arch = "wasm32")] { let source = clusterflux::spawn::async_task(prepare_source) .name("prepare source for identity probes") .start() .await .expect("identity source preparation should launch") .join() .await .expect("identity source preparation should complete"); let slow = clusterflux::spawn::async_task_with_arg( IdentityProbeInput { source: source.clone(), label: "slow-first".to_owned(), delay_seconds: 30, }, identity_probe, ) .name("identity probe slow") .env(linux_env()) .start() .await .expect("slow identity probe should launch"); let fast = clusterflux::spawn::async_task_with_arg( IdentityProbeInput { source, label: "fast-second".to_owned(), delay_seconds: 20, }, identity_probe, ) .name("identity probe fast") .env(linux_env()) .start() .await .expect("fast identity probe should launch"); let fast_result = fast .join() .await .expect("fast identity probe should complete first"); let slow_result = slow .join() .await .expect("slow identity probe should remain independently joinable"); return vec![fast_result, slow_result]; } #[cfg(not(target_arch = "wasm32"))] vec!["fast-second".to_owned(), "slow-first".to_owned()] } pub async fn run_build_workflow() -> BuildReport { let source = clusterflux::spawn::async_task(prepare_source) .name("prepare source snapshot") .start() .await .unwrap() .join() .await .unwrap(); let linux = clusterflux::spawn::async_task_with_arg(source.clone(), compile_linux) .name("compile linux") .env(linux_env()) .start() .await .unwrap(); let linux_parallel = clusterflux::spawn::async_task_with_arg(source.clone(), compile_linux) .name("compile linux in parallel") .env(linux_env()) .start() .await .unwrap(); let linux_thread = linux.virtual_thread_id(); let linux_parallel_thread = linux_parallel.virtual_thread_id(); let linux_artifact = linux.join().await.unwrap(); let linux_parallel_artifact = linux_parallel.join().await.unwrap(); assert_eq!(linux_artifact.digest, linux_parallel_artifact.digest); let package = clusterflux::spawn::async_task_with_arg( PackageInput { release_name: "release.tar".to_owned(), source: source.clone(), executable: Some(linux_artifact.clone()), inputs: vec![linux_artifact.clone()], }, package_release, ) .name("package artifacts") .env(linux_env()) .start() .await .unwrap(); let package_thread = package.virtual_thread_id(); let package_artifact = package.join().await.unwrap(); BuildReport { linux_thread, linux_parallel_thread, package_thread, linux_artifact, package_artifact, source, } } #[cfg(test)] mod tests { use futures_executor::block_on; use super::*; #[test] fn flagship_workflow_is_rust_source_with_spawned_virtual_tasks() { let main_report = block_on(build_main()); assert_eq!( main_report.linux_artifact.id, "hello-clusterflux-native-test" ); assert_eq!(task_add_one(41), 42); assert_eq!(block_on(restart_main()), 42); assert_eq!(block_on(park_wake_main()), 16); assert_eq!(block_on(long_join_main()), 0); assert_eq!( block_on(identity_main()), vec!["fast-second".to_owned(), "slow-first".to_owned()] ); assert_eq!(linux_env().name, "linux"); assert_eq!(windows_env().name, "windows"); let report = block_on(run_build_workflow()); assert_ne!(report.linux_thread, report.package_thread); assert_ne!(report.linux_thread, report.linux_parallel_thread); assert_eq!(report.linux_artifact.id, "hello-clusterflux-native-test"); assert_eq!(report.package_artifact.id, "release-native-test"); assert_eq!( report.source.digest, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ); } }