mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
fixed crdt
This commit is contained in:
@ -1,67 +1,131 @@
|
||||
use rusqlite::{params, Connection, Result};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use uhlc::{Timestamp, HLC};
|
||||
// src/hlc_service.rs
|
||||
|
||||
use rusqlite::{params, Connection, Result as RusqliteResult, Transaction};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
|
||||
use uuid::Uuid;
|
||||
|
||||
const HLC_SETTING_TYPE: &str = "hlc_timestamp";
|
||||
const HLC_NODE_ID_TYPE: &str = "hlc_node_id";
|
||||
const HLC_TIMESTAMP_TYPE: &str = "hlc_timestamp";
|
||||
|
||||
pub const GET_HLC_FUNCTION: &str = "get_hlc_timestamp";
|
||||
pub const CRDT_SETTINGS_TABLE: &str = "haex_crdt_settings";
|
||||
pub struct HlcService(pub Arc<Mutex<HLC>>);
|
||||
|
||||
pub fn setup_hlc(conn: &mut Connection) -> Result<()> {
|
||||
// 1. Lade den letzten HLC-Zustand oder erstelle einen neuen.
|
||||
let hlc = conn
|
||||
.query_row(
|
||||
"SELECT value FROM {CRDT_SETTINGS_TABLE} meta WHERE type = ?1",
|
||||
params![HLC_SETTING_TYPE],
|
||||
|row| {
|
||||
let state_str: String = row.get(0)?;
|
||||
let timestamp = Timestamp::from_str(&state_str)
|
||||
.map_err(|_| rusqlite::Error::ExecuteReturnedResults)?; // Konvertiere den Fehler
|
||||
Ok(HLC::new(timestamp))
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|_| HLC::default()); // Bei Fehler (z.B. nicht gefunden) -> neuen HLC erstellen.
|
||||
|
||||
let hlc_arc = Arc::new(Mutex::new(hlc));
|
||||
|
||||
// 2. Erstelle eine Klon für die SQL-Funktion und speichere den Zustand bei jeder neuen Timestamp-Generierung.
|
||||
let hlc_clone = hlc_arc.clone();
|
||||
let db_conn_arc = Arc::new(Mutex::new(conn.try_clone()?));
|
||||
|
||||
conn.create_scalar_function(
|
||||
GET_HLC_FUNCTION,
|
||||
0,
|
||||
rusqlite::functions::FunctionFlags::SQLITE_UTF8
|
||||
| rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
|
||||
move |_| {
|
||||
let mut hlc = hlc_clone.lock().unwrap();
|
||||
let new_timestamp = hlc.new_timestamp();
|
||||
let timestamp_str = new_timestamp.to_string();
|
||||
|
||||
// 3. Speichere den neuen Zustand sofort zurück in die DB.
|
||||
// UPSERT-Logik: Ersetze den Wert, falls der Schlüssel existiert, sonst füge ihn ein.
|
||||
let db_conn = db_conn_arc.lock().unwrap();
|
||||
db_conn
|
||||
.execute(
|
||||
"INSERT INTO {CRDT_SETTINGS_TABLE} (id, type, value) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(type) DO UPDATE SET value = excluded.value",
|
||||
params![
|
||||
Uuid::new_v4().to_string(), // Generiere eine neue ID für den Fall eines INSERTs
|
||||
HLC_SETTING_TYPE,
|
||||
×tamp_str
|
||||
],
|
||||
)
|
||||
.expect("HLC state could not be persisted."); // In Prod sollte hier ein besseres Error-Handling hin.
|
||||
|
||||
Ok(timestamp_str)
|
||||
},
|
||||
)?;
|
||||
|
||||
// Hinweis: Den HLC-Service im Tauri-State zu managen ist nicht mehr zwingend,
|
||||
// da die SQL-Funktion nun alles Notwendige über geklonte Arcs erhält.
|
||||
// Falls du ihn dennoch für andere Commands brauchst, kannst du ihn im State speichern.
|
||||
|
||||
Ok(())
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HlcError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
#[error("Failed to parse persisted HLC timestamp: {0}")]
|
||||
ParseTimestamp(String),
|
||||
#[error("Failed to parse persisted HLC state: {0}")]
|
||||
Parse(String),
|
||||
#[error("HLC mutex was poisoned")]
|
||||
MutexPoisoned,
|
||||
#[error("Failed to create node ID: {0}")]
|
||||
CreateNodeId(#[from] uhlc::SizeError),
|
||||
}
|
||||
|
||||
/// A thread-safe, persistent HLC service.
|
||||
#[derive(Clone)]
|
||||
pub struct HlcService(Arc<Mutex<HLC>>);
|
||||
|
||||
impl HlcService {
|
||||
/// Creates a new HLC service, initializing it from the database or creating a new
|
||||
/// persistent identity if one does not exist.
|
||||
pub fn new(conn: &mut Connection) -> Result<Self, HlcError> {
|
||||
// 1. Manage persistent node identity.
|
||||
let node_id = Self::get_or_create_node_id(conn)?;
|
||||
|
||||
// 2. Create HLC instance with stable identity using the HLCBuilder.
|
||||
let hlc = HLCBuilder::new()
|
||||
.with_id(node_id)
|
||||
.with_max_delta(Duration::from_secs(1)) // Example of custom configuration
|
||||
.build();
|
||||
|
||||
// 3. Load the last persisted timestamp and update the clock.
|
||||
let last_state_str: RusqliteResult<String> = conn.query_row(
|
||||
&format!("SELECT value FROM {} WHERE type = ?1", CRDT_SETTINGS_TABLE),
|
||||
params![HLC_TIMESTAMP_TYPE],
|
||||
|row| row.get(0),
|
||||
);
|
||||
|
||||
if let Ok(state_str) = last_state_str {
|
||||
let timestamp =
|
||||
Timestamp::from_str(&state_str).map_err(|e| HlcError::ParseTimestamp(e.cause))?;
|
||||
|
||||
// Update the clock with the persisted state.
|
||||
// we might want to handle the error case where the clock drifts too far.
|
||||
hlc.update_with_timestamp(×tamp)
|
||||
.map_err(|e| HlcError::Parse(e.to_string()))?;
|
||||
}
|
||||
|
||||
let hlc_arc = Arc::new(Mutex::new(hlc));
|
||||
Ok(HlcService(hlc_arc))
|
||||
}
|
||||
|
||||
/// Generates a new timestamp and immediately persists the HLC's new state.
|
||||
/// This method MUST be called within an existing database transaction (`tx`)
|
||||
/// along with the actual data operation that this timestamp is for.
|
||||
/// This design ensures atomicity: the data is saved with its timestamp,
|
||||
/// and the clock state is updated, or none of it is.
|
||||
pub fn new_timestamp_and_persist<'tx>(
|
||||
&self,
|
||||
tx: &Transaction<'tx>,
|
||||
) -> Result<Timestamp, HlcError> {
|
||||
let hlc = self.0.lock().map_err(|_| HlcError::MutexPoisoned)?;
|
||||
let new_timestamp = hlc.new_timestamp();
|
||||
let timestamp_str = new_timestamp.to_string();
|
||||
|
||||
tx.execute(
|
||||
&format!(
|
||||
"INSERT INTO {} (type, value) VALUES (?1,?2)
|
||||
ON CONFLICT(type) DO UPDATE SET value = excluded.value",
|
||||
CRDT_SETTINGS_TABLE
|
||||
),
|
||||
params![HLC_TIMESTAMP_TYPE, timestamp_str],
|
||||
)?;
|
||||
|
||||
Ok(new_timestamp)
|
||||
}
|
||||
|
||||
/// Retrieves or creates and persists a stable node ID for the HLC.
|
||||
fn get_or_create_node_id(conn: &mut Connection) -> Result<ID, HlcError> {
|
||||
let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
|
||||
let query = format!("SELECT value FROM {} WHERE type =?1", CRDT_SETTINGS_TABLE);
|
||||
|
||||
match tx.query_row(&query, params![HLC_NODE_ID_TYPE], |row| {
|
||||
row.get::<_, String>(0)
|
||||
}) {
|
||||
Ok(id_str) => {
|
||||
// ID exists, parse and return it.
|
||||
let id_bytes = hex::decode(id_str).map_err(|e| HlcError::Parse(e.to_string()))?;
|
||||
let id = ID::try_from(id_bytes.as_slice())?;
|
||||
tx.commit()?;
|
||||
Ok(id)
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
// No ID found, create, persist, and return a new one.
|
||||
let new_id_bytes = Uuid::new_v4().as_bytes().to_vec();
|
||||
let new_id = ID::try_from(new_id_bytes.as_slice())?;
|
||||
let new_id_str = hex::encode(new_id.to_le_bytes());
|
||||
|
||||
tx.execute(
|
||||
&format!(
|
||||
"INSERT INTO {} (type, value) VALUES (?1, ?2)",
|
||||
CRDT_SETTINGS_TABLE
|
||||
),
|
||||
params![HLC_NODE_ID_TYPE, new_id_str],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(new_id)
|
||||
}
|
||||
Err(e) => Err(HlcError::from(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user