From be41fa839fe99f152d26048675b290599492f16b Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 22 Jun 2026 16:02:16 +0400 Subject: fix: harden resource and world state correctness --- crates/fparkan-world/src/lib.rs | 97 ++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 25 deletions(-) (limited to 'crates/fparkan-world/src') diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs index 58412d9..a253586 100644 --- a/crates/fparkan-world/src/lib.rs +++ b/crates/fparkan-world/src/lib.rs @@ -357,36 +357,45 @@ pub fn step_with_handler( where F: FnMut(&mut World, &WorldCommand) -> Result<(), WorldError>, { + let before = world.clone(); world.phase = WorldPhase::Calculating; let mut events = Vec::new(); - while let Some(command) = world.queue.pop_front() { - if let Some(handle) = command.target { - if world.deferred_delete.contains(&handle) { - continue; + let result = (|| { + while let Some(command) = world.queue.pop_front() { + if let Some(handle) = command.target { + if world.deferred_delete.contains(&handle) { + continue; + } + checked_slot(world, handle)?; } - checked_slot(world, handle)?; + handler(world, &command)?; + events.push(WorldEvent { + sequence: command.sequence, + target: command.target, + }); } - handler(world, &command)?; - events.push(WorldEvent { - sequence: command.sequence, - target: command.target, - }); - } - world.phase = WorldPhase::ApplyingDeferred; - let deletes = std::mem::take(&mut world.deferred_delete); - for handle in deletes { - let _ = delete_now(world, handle); + world.phase = WorldPhase::ApplyingDeferred; + let deletes = std::mem::take(&mut world.deferred_delete); + for handle in deletes { + delete_now(world, handle)?; + } + world.tick.0 = world.tick.0.saturating_add(1); + world.phase = WorldPhase::PublishingSnapshot; + let snapshot = WorldSnapshot { + tick: world.tick, + objects: live_registered(world), + events, + hash: canonical_state_hash(world), + }; + world.phase = WorldPhase::Idle; + Ok(snapshot) + })(); + if let Err(err) = result { + *world = before; + world.phase = WorldPhase::Idle; + return Err(err); } - world.tick.0 = world.tick.0.saturating_add(1); - world.phase = WorldPhase::PublishingSnapshot; - let snapshot = WorldSnapshot { - tick: world.tick, - objects: live_registered(world), - events, - hash: canonical_state_hash(world), - }; - world.phase = WorldPhase::Idle; - Ok(snapshot) + result } /// Computes canonical state hash. @@ -710,6 +719,44 @@ mod tests { ); } + #[test] + fn callback_error_rolls_back_phase_queue_and_deferred_deletes() { + let mut world = new(WorldConfig); + let first = construct_object(&mut world, ObjectDraft { original_id: None }).expect("first"); + register_object(&mut world, first).expect("register"); + enqueue( + &mut world, + WorldCommand { + sequence: 7, + target: Some(first), + }, + ) + .expect("enqueue"); + + let err = step_with_handler(&mut world, &InputSnapshot, |world, _| { + request_delete(world, first)?; + Err(WorldError::InvalidFixedStep) + }) + .expect_err("handler error"); + + assert_eq!(err, WorldError::InvalidFixedStep); + assert_eq!(world.phase, WorldPhase::Idle); + assert_eq!(world.tick, Tick(0)); + assert!(world.deferred_delete.is_empty()); + assert_eq!(world.queue.len(), 1); + + let snapshot = step(&mut world, &InputSnapshot).expect("retry step"); + assert_eq!(snapshot.tick, Tick(1)); + assert_eq!( + snapshot.events, + vec![WorldEvent { + sequence: 0, + target: Some(first) + }] + ); + assert_eq!(snapshot.objects, vec![first]); + } + #[test] fn snapshot_hash_determinism_and_immutability() { let mut left = new(WorldConfig); -- cgit v1.2.3