Skip to main content

wowlab_node/worker/
runner.rs

1//! Simulation runner using the engine's intent-based entry.
2
3use std::sync::Arc;
4
5use prost::Message;
6use wowlab_common::{
7    RuntimeWorkFidelity, RuntimeWorkItem, RuntimeWorkResult,
8    sim::{derive::derive_work_unit_config, intent::SimConfigIntent},
9};
10use wowlab_engine_application::{ApplicationError, simulate_intent};
11use wowlab_engine_ports::{ChunkAssignment, ContentCatalog, DynDataResolver, NoopProgress};
12use wowlab_types::proto::{self, TournamentPayload};
13
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub(crate) enum SimError {
17    #[error("Derive error: {0}")]
18    Derive(#[from] wowlab_common::sim::derive::DeriveError),
19    #[error("Intent error: {0}")]
20    Intent(#[from] ApplicationError),
21    #[error("Intent error: {0}")]
22    TelemetryDecode(#[from] prost::DecodeError),
23}
24
25pub(crate) async fn run_item(
26    catalog: &'static ContentCatalog,
27    base_intent: &SimConfigIntent,
28    payload: &TournamentPayload,
29    item: &RuntimeWorkItem,
30    chunk_index: u32,
31    resolver: Arc<DynDataResolver<'static>>,
32) -> Result<RuntimeWorkResult, SimError> {
33    let config_toml = derive_work_unit_config(base_intent, payload, &item.kind)?;
34    let telemetry_bytes = run_simulation(
35        catalog,
36        &config_toml,
37        item.iterations,
38        item.seed_offset,
39        chunk_index,
40        resolver,
41    )
42    .await?;
43
44    let telemetry = proto::ChunkTelemetry::decode(telemetry_bytes.as_slice())?;
45
46    let telemetry_pb = match item.fidelity {
47        RuntimeWorkFidelity::Full => Some(telemetry_bytes),
48        _ => None,
49    };
50
51    Ok(RuntimeWorkResult {
52        item_id: item.item_id,
53        tag: item.tag,
54        iterations: telemetry.iterations,
55        mean_dps_x10: telemetry.mean_dps_x10,
56        m2_dps_bits: telemetry.m2_dps_bits,
57        telemetry_pb,
58    })
59}
60
61async fn run_simulation(
62    catalog: &'static ContentCatalog,
63    config_toml: &str,
64    iterations: u32,
65    base_seed: u64,
66    chunk_index: u32,
67    resolver: Arc<DynDataResolver<'static>>,
68) -> Result<Vec<u8>, SimError> {
69    let mut chunk = ChunkAssignment::single_indexed("", chunk_index, iterations);
70
71    chunk.job_id = String::new();
72    chunk.permutation_index = None;
73
74    let report = simulate_intent(
75        catalog,
76        config_toml,
77        &chunk,
78        base_seed,
79        resolver.as_ref(),
80        &NoopProgress,
81    )
82    .await?;
83
84    Ok(report.telemetry_bytes)
85}
86
87#[cfg(test)]
88mod tests {
89    use std::error::Error as _;
90
91    use googletest::prelude::*;
92
93    use super::*;
94
95    #[gtest]
96    fn application_error_keeps_node_presentation_and_typed_source() -> Result<()> {
97        let application = ApplicationError::from_engine(
98            wowlab_engine_application::ApplicationStage::Simulation,
99            wowlab_engine_ports::EngineError::simulation_runtime("event queue failed"),
100        );
101        let error = SimError::from(application);
102
103        verify_that!(
104            error.to_string(),
105            eq("Intent error: simulation runtime error: event queue failed")
106        )?;
107
108        verify_true!(
109            error
110                .source()
111                .is_some_and(<dyn std::error::Error>::is::<ApplicationError>)
112        )
113    }
114}