From f5ab2e6e2ef9072c183e78a2c71a4a7719798003 Mon Sep 17 00:00:00 2001 From: Joaquin Bejar Date: Tue, 14 Jul 2026 20:19:11 +0200 Subject: [PATCH] fix(price_level): PriceLevelData round-trip preserves FIFO order The plain serde path built PriceLevelData::orders from iter_orders() -- the unordered DashMap iterator -- so serde_json round-trips through the public PriceLevelData scrambled price-time priority, the exact property the checksum-protected snapshot package has preserved since #109. The vector is now materialized in queue-consumption order (ascending insertion sequence) and TryFrom re-admits in vector order, so the round-trip preserves FIFO including demotions. Docs state the contract and point persistence at the snapshot package (checksummed, carries statistics). Closes #131 --- src/price_level/level.rs | 19 ++++++++++++-- src/price_level/tests/level.rs | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/price_level/level.rs b/src/price_level/level.rs index 38559d2..c617562 100644 --- a/src/price_level/level.rs +++ b/src/price_level/level.rs @@ -2288,7 +2288,16 @@ impl PriceLevel { } } -/// Serializable representation of a price level for easier data transfer and storage +/// Serializable representation of a price level for easier data transfer and storage. +/// +/// The `orders` vector is materialized in **queue-consumption order** +/// (ascending insertion sequence — exactly as `match_order` sweeps), and +/// [`TryFrom`](PriceLevel#impl-TryFrom-for-PriceLevel) +/// re-admits in vector order, so a `PriceLevelData` round-trip preserves +/// price-time (FIFO) priority just like the checksum-protected snapshot +/// package. Unlike the package, this plain representation carries no checksum +/// and no statistics — prefer [`PriceLevel::snapshot_package`] for +/// persistence. #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PriceLevelData { @@ -2311,8 +2320,14 @@ impl From<&PriceLevel> for PriceLevelData { visible_quantity: price_level.visible_quantity(), hidden_quantity: price_level.hidden_quantity(), order_count: price_level.order_count(), + // Consumption (insertion-sequence) order, NOT the unordered DashMap + // iteration: `TryFrom` re-admits in vector order, + // so this is what makes the round-trip preserve price-time / FIFO + // priority (issue #131) — the same contract the snapshot package + // has kept since issue #109. orders: price_level - .iter_orders() + .snapshot_by_insertion_seq() + .into_iter() .map(|order_arc| *order_arc) .collect(), } diff --git a/src/price_level/tests/level.rs b/src/price_level/tests/level.rs index d521cf2..ab3fda8 100644 --- a/src/price_level/tests/level.rs +++ b/src/price_level/tests/level.rs @@ -5687,6 +5687,51 @@ mod tests { ); } + #[test] + fn test_price_level_data_roundtrip_preserves_demotion_order() { + // Issue #131: the plain serde path (PriceLevelData) previously built + // its orders from the UNORDERED DashMap iterator, so a round-trip + // scrambled price-time priority — the exact property the snapshot + // package has preserved since #109. It now materializes in + // consumption order and TryFrom re-admits in vector order. + let level = PriceLevel::new(10_000); + level + .add_order(create_standard_order(1, 10_000, 100)) + .expect("admission"); + level + .add_order(create_standard_order(2, 10_000, 100)) + .expect("admission"); + level + .add_order(create_standard_order(3, 10_000, 100)) + .expect("admission"); + // Upsize maker 1: demoted to the tail with its original timestamp, + // so only consumption order (not timestamp order) captures it. + let updated = level + .update_order(OrderUpdate::UpdateQuantity { + order_id: Id::from_u64(1), + new_quantity: Quantity::new(150), + }) + .expect("upsize update should succeed"); + assert!(updated.is_some(), "maker 1 must still be present"); + + let data = PriceLevelData::from(&level); + let json = serde_json::to_string(&data).expect("serialize PriceLevelData"); + let decoded: PriceLevelData = + serde_json::from_str(&json).expect("deserialize PriceLevelData"); + let restored = PriceLevel::try_from(decoded).expect("re-admission must succeed"); + + let restored_ids: Vec = restored + .snapshot_by_insertion_seq() + .iter() + .map(|o| o.id()) + .collect(); + assert_eq!( + restored_ids, + vec![Id::from_u64(2), Id::from_u64(3), Id::from_u64(1)], + "PriceLevelData round-trip must preserve the demoted consumption order" + ); + } + #[test] fn test_snapshot_restore_preserves_upsize_demotion() { // Issue #109: sizing an order up demotes it to the back of the queue