checkpoint for the demo. almost!!
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -253,6 +253,24 @@ pub fn flush_to_sqlite(ops: &[PersistenceOp], conn: &mut Connection) -> Result<u
|
||||
)?;
|
||||
count += 1;
|
||||
},
|
||||
|
||||
| PersistenceOp::RecordTombstone {
|
||||
entity_id,
|
||||
deleting_node,
|
||||
deletion_clock,
|
||||
} => {
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO tombstones (entity_id, deleting_node, deletion_clock, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![
|
||||
entity_id.as_bytes(),
|
||||
&deleting_node.to_string(),
|
||||
deletion_clock.as_ref(),
|
||||
current_timestamp(),
|
||||
],
|
||||
)?;
|
||||
count += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -977,6 +995,117 @@ pub fn rehydrate_all_entities(world: &mut bevy::prelude::World) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load all tombstones from the database into the TombstoneRegistry
|
||||
///
|
||||
/// This function is called during startup to restore deletion tombstones
|
||||
/// from the database, preventing resurrection of deleted entities after
|
||||
/// application restart.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `world` - The Bevy world containing the TombstoneRegistry resource
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - Database connection fails
|
||||
/// - Tombstone loading fails
|
||||
/// - Vector clock deserialization fails
|
||||
pub fn load_tombstones(world: &mut bevy::prelude::World) -> Result<()> {
|
||||
use bevy::prelude::*;
|
||||
|
||||
// Get database connection and load tombstones
|
||||
let tombstone_rows = {
|
||||
let db_res = world.resource::<crate::persistence::PersistenceDb>();
|
||||
let conn = db_res
|
||||
.conn
|
||||
.lock()
|
||||
.map_err(|e| PersistenceError::Other(format!("Failed to lock database: {}", e)))?;
|
||||
|
||||
// Load all tombstones from database
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT entity_id, deleting_node, deletion_clock, created_at
|
||||
FROM tombstones
|
||||
ORDER BY created_at ASC",
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let entity_id_bytes: std::borrow::Cow<'_, [u8]> = row.get(0)?;
|
||||
let mut entity_id_array = [0u8; 16];
|
||||
entity_id_array.copy_from_slice(&entity_id_bytes);
|
||||
let entity_id = uuid::Uuid::from_bytes(entity_id_array);
|
||||
|
||||
let deleting_node_str: String = row.get(1)?;
|
||||
let deletion_clock_bytes: std::borrow::Cow<'_, [u8]> = row.get(2)?;
|
||||
let created_at_ts: i64 = row.get(3)?;
|
||||
|
||||
Ok((
|
||||
entity_id,
|
||||
deleting_node_str,
|
||||
deletion_clock_bytes.to_vec(),
|
||||
created_at_ts,
|
||||
))
|
||||
})?;
|
||||
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
};
|
||||
|
||||
info!("Loaded {} tombstones from database", tombstone_rows.len());
|
||||
|
||||
if tombstone_rows.is_empty() {
|
||||
info!("No tombstones to restore");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Restore tombstones into TombstoneRegistry
|
||||
let mut loaded_count = 0;
|
||||
let mut failed_count = 0;
|
||||
|
||||
{
|
||||
let mut tombstone_registry = world.resource_mut::<crate::networking::TombstoneRegistry>();
|
||||
|
||||
for (entity_id, deleting_node_str, deletion_clock_bytes, _created_at_ts) in tombstone_rows {
|
||||
// Parse node ID
|
||||
let deleting_node = match uuid::Uuid::parse_str(&deleting_node_str) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("Failed to parse deleting_node UUID for entity {:?}: {}", entity_id, e);
|
||||
failed_count += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Deserialize vector clock
|
||||
let deletion_clock = match rkyv::from_bytes::<crate::networking::VectorClock, rkyv::rancor::Failure>(&deletion_clock_bytes) {
|
||||
Ok(clock) => clock,
|
||||
Err(e) => {
|
||||
error!("Failed to deserialize vector clock for tombstone {:?}: {:?}", entity_id, e);
|
||||
failed_count += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Record the tombstone in the registry
|
||||
tombstone_registry.record_deletion(entity_id, deleting_node, deletion_clock);
|
||||
loaded_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Tombstone restoration complete: {} succeeded, {} failed",
|
||||
loaded_count, failed_count
|
||||
);
|
||||
|
||||
if failed_count > 0 {
|
||||
warn!(
|
||||
"{} tombstones failed to restore - check logs for details",
|
||||
failed_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -29,6 +29,11 @@ pub const MIGRATIONS: &[Migration] = &[
|
||||
name: "sessions",
|
||||
up: include_str!("migrations/004_sessions.sql"),
|
||||
},
|
||||
Migration {
|
||||
version: 5,
|
||||
name: "tombstones",
|
||||
up: include_str!("migrations/005_tombstones.sql"),
|
||||
},
|
||||
];
|
||||
|
||||
/// Initialize the migrations table
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Migration 005: Add tombstones table
|
||||
-- Stores deletion tombstones to prevent resurrection of deleted entities
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tombstones (
|
||||
entity_id BLOB PRIMARY KEY,
|
||||
deleting_node TEXT NOT NULL,
|
||||
deletion_clock BLOB NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Index for querying tombstones by session (for future session scoping)
|
||||
CREATE INDEX IF NOT EXISTS idx_tombstones_created
|
||||
ON tombstones(created_at DESC);
|
||||
@@ -92,10 +92,11 @@ impl Plugin for PersistencePlugin {
|
||||
.init_resource::<ComponentTypeRegistryResource>();
|
||||
|
||||
// Add startup systems
|
||||
// First initialize the database, then rehydrate entities
|
||||
// First initialize the database, then rehydrate entities and tombstones
|
||||
app.add_systems(Startup, (
|
||||
persistence_startup_system,
|
||||
rehydrate_entities_system,
|
||||
load_tombstones_system,
|
||||
).chain());
|
||||
|
||||
// Add systems in the appropriate schedule
|
||||
@@ -168,7 +169,43 @@ fn persistence_startup_system(db: Res<PersistenceDb>, mut metrics: ResMut<Persis
|
||||
/// This system runs after `persistence_startup_system` and loads all entities
|
||||
/// from SQLite, deserializing and spawning them into the Bevy world with all
|
||||
/// their components.
|
||||
///
|
||||
/// **Important**: Only rehydrates entities when rejoining an existing session.
|
||||
/// New sessions start with 0 entities to avoid loading entities from previous
|
||||
/// sessions.
|
||||
fn rehydrate_entities_system(world: &mut World) {
|
||||
// Check if we're rejoining an existing session
|
||||
let should_rehydrate = {
|
||||
let current_session = world.get_resource::<crate::networking::CurrentSession>();
|
||||
match current_session {
|
||||
Some(session) => {
|
||||
// Only rehydrate if we have a last_known_clock (indicates we're rejoining)
|
||||
let is_rejoin = session.last_known_clock.node_count() > 0;
|
||||
if is_rejoin {
|
||||
info!(
|
||||
"Rejoining session {} - will rehydrate persisted entities",
|
||||
session.session.id.to_code()
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"New session {} - starting with 0 entities",
|
||||
session.session.id.to_code()
|
||||
);
|
||||
}
|
||||
is_rejoin
|
||||
}
|
||||
None => {
|
||||
warn!("No CurrentSession found - skipping entity rehydration");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !should_rehydrate {
|
||||
info!("Skipping entity rehydration for new session");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = crate::persistence::database::rehydrate_all_entities(world) {
|
||||
error!("Failed to rehydrate entities from database: {}", e);
|
||||
} else {
|
||||
@@ -176,6 +213,19 @@ fn rehydrate_entities_system(world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exclusive startup system to load tombstones from database
|
||||
///
|
||||
/// This system runs after `rehydrate_entities_system` and loads all tombstones
|
||||
/// from SQLite, deserializing them into the TombstoneRegistry to prevent
|
||||
/// resurrection of deleted entities.
|
||||
fn load_tombstones_system(world: &mut World) {
|
||||
if let Err(e) = crate::persistence::database::load_tombstones(world) {
|
||||
error!("Failed to load tombstones from database: {}", e);
|
||||
} else {
|
||||
info!("Successfully loaded tombstones from database");
|
||||
}
|
||||
}
|
||||
|
||||
/// System to collect dirty entities using Bevy's change detection
|
||||
///
|
||||
/// This system tracks changes to the `Persisted` component. When `Persisted` is
|
||||
|
||||
@@ -126,6 +126,13 @@ pub enum PersistenceOp {
|
||||
entity_id: EntityId,
|
||||
component_type: String,
|
||||
},
|
||||
|
||||
/// Record a tombstone for a deleted entity
|
||||
RecordTombstone {
|
||||
entity_id: EntityId,
|
||||
deleting_node: NodeId,
|
||||
deletion_clock: bytes::Bytes, // Serialized VectorClock
|
||||
},
|
||||
}
|
||||
|
||||
impl PersistenceOp {
|
||||
|
||||
Reference in New Issue
Block a user