v0.3.7-pre.009

This commit is contained in:
2026-09-02 17:40:03 +02:00
parent ed72aaba67
commit ec8875e864
15 changed files with 699 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 6
// version: 7
//! Shared backend state owned by the Backfill Desk Tauri application.
@@ -225,6 +225,7 @@ impl crate::AppState {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = runtime.handle();
let snapshots = handle.snapshots();
let installed = self.backfill_runs.install(job_id.clone(), handle);
if let std::result::Result::Err(error) = installed {
return std::result::Result::Err(error);
@@ -260,7 +261,20 @@ impl crate::AppState {
job_id = job_id.as_str(),
"admitted Backfill Desk run and installed control handle before async spawn"
);
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, store, transport });
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
}
/// Returns the current active or retained terminal Backfill status for frontend resynchronization.
pub(crate) fn backfill_status(&self) -> ksp_core_lib::Result<std::option::Option<crate::BackfillRunStatusDto>> {
let notification = self.backfill_runs.current_notification();
let notification = match notification {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match notification {
std::option::Option::Some(value) => crate::project_backfill_status(&value).map(std::option::Option::Some),
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
// version: 1
// version: 2
//! Single-active-run admission and launch ownership for Backfill Desk.
@@ -9,6 +9,8 @@ pub(crate) struct BackfillRunLaunch {
pub(crate) job_id: ksp_job_api::JobId,
/// Concrete Backfill runtime owning discovery, hydration, persistence and latest-value publication.
pub(crate) runtime: ksp_job_backfill_lib::BackfillJobRuntime,
/// Independent latest-value source retained by the monitoring bridge.
pub(crate) snapshots: ksp_job_backfill_lib::BackfillSnapshotSource,
/// Store facade temporarily borrowed from the application Store runtime for the duration of this run.
pub(crate) store: ksp_store_lib::Store,
/// Shareable HTTP Transport pool selected by the active composite profile.
@@ -26,6 +28,7 @@ impl crate::BackfillRunLaunch {
/// Single-run control slot retained by the application while one Backfill runtime is active.
pub(crate) struct BackfillRunState {
active: std::sync::Mutex<std::option::Option<ActiveBackfillRun>>,
last_terminal: std::sync::Mutex<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>>,
next_sequence: std::sync::atomic::AtomicU64,
}
@@ -35,6 +38,7 @@ impl crate::BackfillRunState {
pub(crate) const fn new() -> Self {
return Self {
active: std::sync::Mutex::new(std::option::Option::None),
last_terminal: std::sync::Mutex::new(std::option::Option::None),
next_sequence: std::sync::atomic::AtomicU64::new(1),
};
}
@@ -107,10 +111,58 @@ impl crate::BackfillRunState {
},
};
let cancellation_requested = current.handle.is_cancellation_requested();
let source = current.handle.snapshots();
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
if notification.state().is_terminal() {
let terminal = self.last_terminal.lock();
let mut terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal monitoring state lock is poisoned",
));
},
};
*terminal = std::option::Option::Some(notification);
}
*active = std::option::Option::None;
return std::result::Result::Ok(cancellation_requested);
}
/// Returns the complete current active snapshot or the retained latest terminal snapshot for resynchronization.
pub(crate) fn current_notification(
&self,
) -> ksp_core_lib::Result<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>> {
let active = self.active.lock();
let active = match active {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk active-run monitoring state lock is poisoned",
));
},
};
if let std::option::Option::Some(current) = active.as_ref() {
let source = current.handle.snapshots();
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
return std::result::Result::Ok(std::option::Option::Some(notification));
}
drop(active);
let terminal = self.last_terminal.lock();
let terminal = match terminal {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Backfill Desk terminal monitoring state lock is poisoned",
));
},
};
return std::result::Result::Ok(terminal.clone());
}
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
pub(crate) fn rollback(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<()> {
let finished = self.finish(job_id);

View File

@@ -0,0 +1,194 @@
// file: crates/ksp-app-backfill-desk/src/backfill_status.rs
// version: 1
//! Safe latest-value monitoring projection for one Backfill Desk run.
use ts_rs::TS; // rust-rules: trait-import
/// Stable Tauri event carrying the latest complete safe Backfill run status.
pub(crate) const BACKFILL_STATUS_EVENT_NAME: &str = "ksp-backfill-status";
/// Frontend-safe latest-value projection of one concrete Backfill runtime snapshot.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts")]
pub(crate) struct BackfillRunStatusDto {
/// Whether the represented Job is currently non-terminal.
pub(crate) active: bool,
/// Number of candidates admitted into hydration.
pub(crate) candidates_admitted: u32,
/// Number of admitted candidates that reached a known coordinator result.
pub(crate) candidates_finished: u32,
/// Number of candidates selected by discovery.
pub(crate) candidates_selected: u32,
/// Number of candidates cancelled before Store submission.
pub(crate) cancelled_candidates: u32,
/// Whether a safe caller-owned checkpoint currently exists.
pub(crate) checkpoint_present: bool,
/// Normal terminal completion classification, when applicable.
pub(crate) completion: std::option::Option<String>,
/// Safe contiguous completion prefix retained by the current checkpoint frontier.
pub(crate) contiguous_completed: u32,
/// Number of Store content conflicts observed by the run.
pub(crate) conflicts: u32,
/// Safe discovery boundary code after discovery completes.
pub(crate) discovery_boundary: std::option::Option<String>,
/// Number of identical canonical RAW transactions already present.
pub(crate) entities_existing: u32,
/// Number of newly inserted canonical RAW transactions.
pub(crate) entities_inserted: u32,
/// Number of durable purge tombstones respected by normal Backfill persistence.
pub(crate) entities_purged: u32,
/// Stable fatal error code retained by a failed Job, when applicable.
pub(crate) failure_code: std::option::Option<String>,
/// Stable fatal error domain retained by a failed Job, when applicable.
pub(crate) failure_domain: std::option::Option<String>,
/// Number of known candidate outcomes blocking the contiguous frontier.
pub(crate) holes: u32,
/// Backend-generated logical Job identity.
pub(crate) job_id: String,
/// Stable concrete Job family code.
pub(crate) job_kind: String,
/// Greatest observed candidate concurrency.
pub(crate) maximum_in_flight: u32,
/// Number of `getTransaction = null` candidates.
pub(crate) missing: u32,
/// Number of acquisition observations already durable.
pub(crate) observations_existing: u32,
/// Number of newly inserted acquisition observations.
pub(crate) observations_inserted: u32,
/// Stable concrete Backfill runtime phase.
pub(crate) phase: String,
/// Monotone latest-value notification sequence encoded as decimal text.
pub(crate) sequence: String,
/// Stable Backfill scope category without address/signature payloads.
pub(crate) scope_kind: String,
/// Stable generic Job lifecycle state.
pub(crate) state: String,
/// Whether this latest value is terminal.
pub(crate) terminal: bool,
}
/// Projects one complete Backfill latest-value notification to the safe desktop monitoring contract.
pub(crate) fn project_backfill_status(
notification: &ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
) -> ksp_core_lib::Result<BackfillRunStatusDto> {
let snapshot = notification.snapshot();
let state = notification.state();
let failure = snapshot.failure_code();
let candidates_admitted = usize_to_u32(snapshot.candidates_admitted(), "candidates_admitted");
let candidates_admitted = match candidates_admitted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates_finished = usize_to_u32(snapshot.candidates_finished(), "candidates_finished");
let candidates_finished = match candidates_finished {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates_selected = usize_to_u32(snapshot.candidates_selected(), "candidates_selected");
let candidates_selected = match candidates_selected {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let cancelled_candidates = usize_to_u32(snapshot.cancelled_candidates(), "cancelled_candidates");
let cancelled_candidates = match cancelled_candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let contiguous_completed = usize_to_u32(snapshot.contiguous_completed(), "contiguous_completed");
let contiguous_completed = match contiguous_completed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let conflicts = usize_to_u32(snapshot.conflicts(), "conflicts");
let conflicts = match conflicts {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_existing = usize_to_u32(snapshot.entities_existing(), "entities_existing");
let entities_existing = match entities_existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_inserted = usize_to_u32(snapshot.entities_inserted(), "entities_inserted");
let entities_inserted = match entities_inserted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entities_purged = usize_to_u32(snapshot.entities_purged(), "entities_purged");
let entities_purged = match entities_purged {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let holes = usize_to_u32(snapshot.holes(), "holes");
let holes = match holes {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let maximum_in_flight = usize_to_u32(snapshot.maximum_in_flight(), "maximum_in_flight");
let maximum_in_flight = match maximum_in_flight {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let missing = usize_to_u32(snapshot.missing(), "missing");
let missing = match missing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observations_existing = usize_to_u32(snapshot.observations_existing(), "observations_existing");
let observations_existing = match observations_existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observations_inserted = usize_to_u32(snapshot.observations_inserted(), "observations_inserted");
let observations_inserted = match observations_inserted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(BackfillRunStatusDto {
active: !state.is_terminal(),
candidates_admitted,
candidates_finished,
candidates_selected,
cancelled_candidates,
checkpoint_present: snapshot.checkpoint().is_some(),
completion: state.completion().map(|completion| return completion.code().to_owned()),
contiguous_completed,
conflicts,
discovery_boundary: snapshot.discovery_boundary().map(|boundary| return boundary.code().to_owned()),
entities_existing,
entities_inserted,
entities_purged,
failure_code: failure.map(|code| return code.code().to_owned()),
failure_domain: failure.map(|code| return code.domain().to_owned()),
holes,
job_id: notification.id().as_str().to_owned(),
job_kind: notification.kind().as_str().to_owned(),
maximum_in_flight,
missing,
observations_existing,
observations_inserted,
phase: snapshot.phase().code().to_owned(),
sequence: notification.sequence().value().to_string(),
scope_kind: snapshot.scope_kind().code().to_owned(),
state: state.code().to_owned(),
terminal: state.is_terminal(),
});
}
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
let converted = u32::try_from(value);
return match converted {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a bounded Job counter to the frontend")
.with_context("field", field)
.with_source(error),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/backfill_status.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs
// version: 7
// version: 8
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
@@ -10,6 +10,7 @@
mod app_state;
mod backfill_request;
mod backfill_run;
mod backfill_status;
mod bootstrap;
mod constants;
mod dto_backfill;
@@ -37,6 +38,12 @@ pub(crate) use self::backfill_request::project_backfill_request;
pub(crate) use self::backfill_run::BackfillRunLaunch;
/// Single-active-run admission state retained by the application.
pub(crate) use self::backfill_run::BackfillRunState;
/// Stable Tauri event carrying coalesced latest-value Backfill status.
pub(crate) use self::backfill_status::BACKFILL_STATUS_EVENT_NAME;
/// Frontend-safe latest-value Backfill runtime projection.
pub(crate) use self::backfill_status::BackfillRunStatusDto;
/// Projects one complete Job notification to the safe desktop monitoring contract.
pub(crate) use self::backfill_status::project_backfill_status;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.

View File

@@ -1,8 +1,9 @@
// file: crates/ksp-app-backfill-desk/src/tauri.rs
// version: 6
// version: 7
//! Tauri runtime assembly for the KSP Backfill desktop application.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Runs the Backfill desktop application.
@@ -78,6 +79,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
return builder.invoke_handler(tauri::generate_handler![
backfill_options,
backfill_start,
backfill_status,
backfill_validate_request,
emit_frontend_log,
get_runtime_status,
@@ -179,6 +181,11 @@ fn backfill_start(
},
};
let response = launch.response();
let monitor_app = app.clone();
let monitor_source = launch.snapshots.clone();
let _monitor_task = tauri::async_runtime::spawn(async move {
monitor_backfill_status(monitor_app, monitor_source).await;
});
let _run_task = tauri::async_runtime::spawn(async move {
let state = app.state::<crate::AppState>();
let executed = state.execute_backfill_run(launch).await;
@@ -195,6 +202,51 @@ fn backfill_start(
return std::result::Result::Ok(response);
}
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
let mut notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
loop {
let projected = crate::project_backfill_status(&notification);
match projected {
std::result::Result::Ok(status) => {
let main = app.get_webview_window("main");
if let std::option::Option::Some(window) = main {
let emitted = window.emit(crate::BACKFILL_STATUS_EVENT_NAME, status);
if emitted.is_err() {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
"Backfill Desk could not emit latest-value monitoring status to the main window"
);
}
}
},
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk could not project latest-value monitoring status"
);
},
}
if notification.state().is_terminal() {
return;
}
let observed = notification.sequence();
notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::wait_for_change(&source, observed).await;
}
}
#[tauri::command]
fn backfill_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::option::Option<crate::BackfillRunStatusDto>, crate::CommandErrorDto> {
let result = state.backfill_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("backfill_status", crate::TRACING_DOMAIN_RUN, &error)),
};
}
#[tauri::command]
fn backfill_validate_request(
request: crate::BackfillStartRequestDto,