v0.3.7-pre.008
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -121,10 +121,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
|
||||
<button id="validateBackfillRequest" class="btn btn-primary" type="submit" disabled>
|
||||
<button id="validateBackfillRequest" class="btn btn-outline-primary" type="submit" disabled>
|
||||
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
|
||||
</button>
|
||||
<span class="text-body-secondary small">Cette tranche ne démarre aucun job.</span>
|
||||
<button id="startBackfillRequest" class="btn btn-primary" type="button" disabled>
|
||||
<i class="fa-solid fa-play me-2" aria-hidden="true"></i>Démarrer le backfill
|
||||
</button>
|
||||
<span class="text-body-secondary small">Un seul run peut être actif ; monitoring détaillé en pre.009.</span>
|
||||
</div>
|
||||
</form>
|
||||
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -7,6 +7,7 @@ import "simplebar";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
|
||||
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
|
||||
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts";
|
||||
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
|
||||
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
|
||||
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
|
||||
@@ -35,6 +36,9 @@ const scopeLabels: Record<string, string> = {
|
||||
explicit_signatures: "Signatures explicites",
|
||||
};
|
||||
|
||||
let compositionReadyForStart = false;
|
||||
let runStartAccepted = false;
|
||||
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "backfill" || value === "diagnostics";
|
||||
@@ -125,6 +129,13 @@ function applyNumberInputContract(inputId: string, maximum: number, defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
function refreshStartButton(): void {
|
||||
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||||
if (startButton) {
|
||||
startButton.disabled = !compositionReadyForStart || runStartAccepted;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
|
||||
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
|
||||
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
|
||||
@@ -293,6 +304,31 @@ function renderRequestValidationError(caughtError: unknown): void {
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
async function startBackfillRequest(): Promise<void> {
|
||||
const request = buildBackfillStartRequest();
|
||||
frontendDebug("main", "Backfill Desk campaign Start requested", {
|
||||
commitment: request.commitment,
|
||||
httpRole: request.httpRole,
|
||||
scopeKind: request.scopeKind,
|
||||
});
|
||||
try {
|
||||
const started = await invokeKsp<BackfillStartResponseDto>("main", "backfill_start", { request });
|
||||
runStartAccepted = true;
|
||||
refreshStartButton();
|
||||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||
if (status) {
|
||||
status.hidden = false;
|
||||
status.classList.remove("alert-secondary", "alert-danger");
|
||||
status.classList.add("alert-success");
|
||||
status.textContent = `Job ${started.jobId} démarré (${started.state}). Le monitoring détaillé arrive en pre.009.`;
|
||||
}
|
||||
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
|
||||
} catch (caughtError) {
|
||||
renderRequestValidationError(caughtError);
|
||||
frontendWarn("main", "Backfill Desk campaign Start failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function validateBackfillRequest(): Promise<void> {
|
||||
const request = buildBackfillStartRequest();
|
||||
frontendDebug("main", "Backfill Desk campaign validation requested", {
|
||||
@@ -331,6 +367,12 @@ function bindCampaignForm(): void {
|
||||
void validateBackfillRequest();
|
||||
});
|
||||
}
|
||||
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||||
if (startButton) {
|
||||
startButton.addEventListener("click", () => {
|
||||
void startBackfillRequest();
|
||||
});
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk campaign form handlers installed");
|
||||
}
|
||||
|
||||
@@ -342,7 +384,9 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||
}
|
||||
|
||||
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
|
||||
compositionReadyForStart = options.compositionReady;
|
||||
renderCampaignContract(options);
|
||||
refreshStartButton();
|
||||
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||||
if (select) {
|
||||
const previous = select.value;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Shared backend state owned by the Backfill Desk Tauri application.
|
||||
|
||||
/// Shared Backfill Desk application state managed by Tauri.
|
||||
pub(crate) struct AppState {
|
||||
backfill_runs: crate::BackfillRunState,
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
|
||||
shutdown_started: std::sync::atomic::AtomicBool,
|
||||
@@ -76,6 +77,7 @@ impl crate::AppState {
|
||||
"resolved Backfill Desk splash timings"
|
||||
);
|
||||
return std::result::Result::Ok(Self {
|
||||
backfill_runs: crate::BackfillRunState::new(),
|
||||
config_management,
|
||||
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
|
||||
guard: logging_startup.guard,
|
||||
@@ -121,7 +123,7 @@ impl crate::AppState {
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.007-request-mapping".to_owned(),
|
||||
shell_phase: "pre.008-runtime-start".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
@@ -194,6 +196,103 @@ impl crate::AppState {
|
||||
return std::result::Result::Ok(preview);
|
||||
}
|
||||
|
||||
/// Prepares one concrete Backfill runtime, installs its control handle atomically and lends execution resources before spawn.
|
||||
pub(crate) fn prepare_backfill_start(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
|
||||
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||
"Backfill Desk cannot start a Backfill run while application shutdown is in progress",
|
||||
));
|
||||
}
|
||||
let options = self.backfill_options();
|
||||
let options = match options {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let job_id = self.backfill_runs.next_job_id();
|
||||
let job_id = match job_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mapped = crate::map_backfill_request(request, &options, job_id.clone());
|
||||
let mapped = match mapped {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
|
||||
let runtime = match runtime {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let handle = runtime.handle();
|
||||
let installed = self.backfill_runs.install(job_id.clone(), handle);
|
||||
if let std::result::Result::Err(error) = installed {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let transport = self.transport_runtime.as_ref();
|
||||
let transport = match transport {
|
||||
std::option::Option::Some(value) => value.pool(),
|
||||
std::option::Option::None => {
|
||||
let rollback = self.backfill_runs.rollback(&job_id);
|
||||
if let std::result::Result::Err(error) = rollback {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||
"Backfill Desk cannot start a Backfill run without a ready HTTP Transport pool",
|
||||
));
|
||||
},
|
||||
};
|
||||
let store = self.store_startup.take_for_run();
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let rollback = self.backfill_runs.rollback(&job_id);
|
||||
if let std::result::Result::Err(rollback_error) = rollback {
|
||||
return std::result::Result::Err(rollback_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_RUN,
|
||||
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 });
|
||||
}
|
||||
|
||||
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.
|
||||
pub(crate) async fn execute_backfill_run(&self, launch: crate::BackfillRunLaunch) -> ksp_core_lib::Result<()> {
|
||||
let job_id = launch.job_id.clone();
|
||||
let result = launch.runtime.run(&launch.transport, &launch.store).await;
|
||||
let restore = self.store_startup.restore_after_run(launch.store);
|
||||
let finished = self.backfill_runs.finish(&job_id);
|
||||
let cancellation_requested = match finished {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = restore {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return match result {
|
||||
std::result::Result::Ok(snapshot) => {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_RUN,
|
||||
cancellation_requested = cancellation_requested,
|
||||
contiguous_completed = snapshot.contiguous_completed(),
|
||||
job_id = job_id.as_str(),
|
||||
phase = snapshot.phase().code(),
|
||||
"Backfill Desk run reached a terminal snapshot and released the single-run slot"
|
||||
);
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
|
||||
pub(crate) fn begin_shutdown(&self) -> bool {
|
||||
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();
|
||||
|
||||
131
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal file
131
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||
// version: 1
|
||||
|
||||
//! Single-active-run admission and launch ownership for Backfill Desk.
|
||||
|
||||
/// Fully prepared Backfill execution moved into the Tauri async runtime after admission.
|
||||
pub(crate) struct BackfillRunLaunch {
|
||||
/// Backend-generated logical Job identity.
|
||||
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,
|
||||
/// 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.
|
||||
pub(crate) transport: ksp_onchain_transport_lib::HttpTransportPool,
|
||||
}
|
||||
|
||||
impl crate::BackfillRunLaunch {
|
||||
/// Builds the safe acknowledgement returned immediately after the non-blocking Start admission.
|
||||
#[must_use]
|
||||
pub(crate) fn response(&self) -> crate::BackfillStartResponseDto {
|
||||
return crate::BackfillStartResponseDto { job_id: self.job_id.as_str().to_owned(), state: ksp_job_api::JobState::Created.code().to_owned() };
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
next_sequence: std::sync::atomic::AtomicU64,
|
||||
}
|
||||
|
||||
impl crate::BackfillRunState {
|
||||
/// Creates an empty single-run state for one desktop application session.
|
||||
#[must_use]
|
||||
pub(crate) const fn new() -> Self {
|
||||
return Self {
|
||||
active: std::sync::Mutex::new(std::option::Option::None),
|
||||
next_sequence: std::sync::atomic::AtomicU64::new(1),
|
||||
};
|
||||
}
|
||||
|
||||
/// Allocates one bounded backend-owned Job identity without accepting caller-supplied identity material.
|
||||
pub(crate) fn next_job_id(&self) -> ksp_core_lib::Result<ksp_job_api::JobId> {
|
||||
let sequence = self
|
||||
.next_sequence
|
||||
.fetch_update(std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire, |current| return current.checked_add(1));
|
||||
let sequence = match sequence {
|
||||
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_INVALID,
|
||||
"Backfill Desk Job identity sequence is exhausted",
|
||||
));
|
||||
},
|
||||
};
|
||||
return ksp_job_api::JobId::new(format!("backfill-desk-{sequence}"));
|
||||
}
|
||||
|
||||
/// Installs one control handle atomically and rejects concurrent Starts while another run owns the slot.
|
||||
pub(crate) fn install(&self, job_id: ksp_job_api::JobId, handle: ksp_job_backfill_lib::BackfillJobHandle) -> ksp_core_lib::Result<()> {
|
||||
let active = self.active.lock();
|
||||
let mut 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 state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
if active.is_some() {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
|
||||
"Backfill Desk already has an active Backfill run",
|
||||
));
|
||||
}
|
||||
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id });
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Clears the matching terminal run and reports whether cancellation had been requested on its retained handle.
|
||||
pub(crate) fn finish(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<bool> {
|
||||
let active = self.active.lock();
|
||||
let mut 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 state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
let current = active.as_ref();
|
||||
let current = match current {
|
||||
std::option::Option::Some(value) if &value.job_id == job_id => value,
|
||||
std::option::Option::Some(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Backfill Desk terminal run does not match the active Job identity",
|
||||
));
|
||||
},
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Backfill Desk terminal run has no active admission slot",
|
||||
));
|
||||
},
|
||||
};
|
||||
let cancellation_requested = current.handle.is_cancellation_requested();
|
||||
*active = std::option::Option::None;
|
||||
return std::result::Result::Ok(cancellation_requested);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
return match finished {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveBackfillRun {
|
||||
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
||||
job_id: ksp_job_api::JobId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/backfill_run.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/constants.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Application-owned tracing targets and domains.
|
||||
|
||||
@@ -23,6 +23,8 @@ pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by Backfill campaign admission and request mapping.
|
||||
pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
|
||||
/// Structured domain used by concrete Backfill run admission and execution.
|
||||
pub(crate) const TRACING_DOMAIN_RUN: &str = "backfill.run";
|
||||
/// Structured domain used by the Backfill Desk shell.
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
|
||||
/// Structured domain used by Store readiness and shutdown operations.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
|
||||
|
||||
@@ -65,6 +65,17 @@ pub(crate) struct BackfillStartRequestDto {
|
||||
pub(crate) scope_kind: String,
|
||||
}
|
||||
|
||||
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts")]
|
||||
pub(crate) struct BackfillStartResponseDto {
|
||||
/// Backend-generated bounded Job identifier for this in-session run.
|
||||
pub(crate) job_id: String,
|
||||
/// Initial lifecycle state at the time Start returns to the frontend.
|
||||
pub(crate) state: String,
|
||||
}
|
||||
|
||||
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
|
||||
///
|
||||
/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/errors.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
|
||||
|
||||
@@ -12,6 +12,8 @@ pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorC
|
||||
ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
|
||||
/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
|
||||
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
|
||||
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
||||
pub(crate) const ERROR_CODE_BACKFILL_RUN_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_active");
|
||||
/// Backfill Desk composite configuration is missing or references an unexpected document.
|
||||
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "config_composite_invalid");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
mod app_state;
|
||||
mod backfill_request;
|
||||
mod backfill_run;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_backfill;
|
||||
@@ -32,6 +33,10 @@ pub(crate) use self::app_state::AppState;
|
||||
pub(crate) use self::backfill_request::map_backfill_request;
|
||||
/// Projects one validated Backfill request without returning address or signature values.
|
||||
pub(crate) use self::backfill_request::project_backfill_request;
|
||||
/// Fully prepared Backfill launch moved into the async runtime after single-run admission.
|
||||
pub(crate) use self::backfill_run::BackfillRunLaunch;
|
||||
/// Single-active-run admission state retained by the application.
|
||||
pub(crate) use self::backfill_run::BackfillRunState;
|
||||
/// 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.
|
||||
@@ -62,6 +67,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain used by Backfill campaign admission and request mapping.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
|
||||
/// Structured domain used by concrete Backfill run admission and execution.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_RUN;
|
||||
/// Structured domain used by the Backfill Desk shell.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used by Store readiness and shutdown operations.
|
||||
@@ -84,6 +91,8 @@ pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
|
||||
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
|
||||
/// App-owned campaign request received from the frontend.
|
||||
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
|
||||
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
||||
pub(crate) use self::dto_backfill::BackfillStartResponseDto;
|
||||
/// Returns commitment codes admitted by the current Backfill contract.
|
||||
pub(crate) use self::dto_backfill::backfill_commitment_codes;
|
||||
/// Builds frontend-safe campaign limits from Job-owned public constants.
|
||||
@@ -106,6 +115,8 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
|
||||
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
|
||||
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
|
||||
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
||||
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_ACTIVE;
|
||||
/// Backfill Desk composite configuration is missing or references an unexpected document.
|
||||
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
|
||||
/// Frontend logging requested an unsupported level.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/store_runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Composite-selected Store readiness and shutdown lifecycle owned by Backfill Desk.
|
||||
|
||||
@@ -21,6 +21,36 @@ impl StoreStartup {
|
||||
options.composition_ready = options.transport_ready && options.network_coherent && options.store_ready;
|
||||
}
|
||||
|
||||
/// Temporarily lends the ready Store facade to one admitted Backfill execution.
|
||||
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
let runtime = match runtime {
|
||||
std::option::Option::Some(value) if value.health_ready() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||
"Backfill Desk cannot start a Backfill run without a ready Store",
|
||||
));
|
||||
},
|
||||
};
|
||||
return runtime.take_for_run();
|
||||
}
|
||||
|
||||
/// Restores the Store facade after one Backfill execution reaches its terminal result.
|
||||
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
let runtime = match runtime {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Backfill Desk cannot restore Store after runtime ownership disappeared",
|
||||
));
|
||||
},
|
||||
};
|
||||
return runtime.restore_after_run(store);
|
||||
}
|
||||
|
||||
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
@@ -46,6 +76,44 @@ impl StoreRuntime {
|
||||
return self.health_ready;
|
||||
}
|
||||
|
||||
/// Temporarily removes the Store from the application slot for one single-active Backfill execution.
|
||||
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
|
||||
let store = take_store(&self.store);
|
||||
let store = match store {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match store {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
|
||||
"Backfill Desk Store is already owned by an active Backfill run",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Restores the Store after one terminal Backfill execution so a later campaign can be admitted.
|
||||
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
|
||||
let locked = self.store.lock();
|
||||
let mut locked = match locked {
|
||||
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 Store runtime state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
if locked.is_some() {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"Backfill Desk cannot restore Store into an occupied runtime slot",
|
||||
));
|
||||
}
|
||||
*locked = std::option::Option::Some(store);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Explicitly closes the Store exactly once through its backend-neutral facade.
|
||||
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||
let store = take_store(&self.store);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
||||
|
||||
@@ -77,6 +77,7 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
backfill_options,
|
||||
backfill_start,
|
||||
backfill_validate_request,
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
@@ -164,6 +165,36 @@ fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Re
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn backfill_start(
|
||||
app: tauri::AppHandle,
|
||||
request: crate::BackfillStartRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::BackfillStartResponseDto, crate::CommandErrorDto> {
|
||||
let launch = state.prepare_backfill_start(request);
|
||||
let launch = match launch {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(project_command_error("backfill_start", crate::TRACING_DOMAIN_RUN, &error));
|
||||
},
|
||||
};
|
||||
let response = launch.response();
|
||||
let _run_task = tauri::async_runtime::spawn(async move {
|
||||
let state = app.state::<crate::AppState>();
|
||||
let executed = state.execute_backfill_run(launch).await;
|
||||
if let std::result::Result::Err(error) = executed {
|
||||
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 async run terminated with an error"
|
||||
);
|
||||
}
|
||||
});
|
||||
return std::result::Result::Ok(response);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn backfill_validate_request(
|
||||
request: crate::BackfillStartRequestDto,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
|
||||
|
||||
@@ -10,6 +10,12 @@ pub(crate) struct TransportRuntime {
|
||||
}
|
||||
|
||||
impl TransportRuntime {
|
||||
/// Returns a shareable clone of the Transport-owned HTTP pool for one admitted Backfill execution.
|
||||
#[must_use]
|
||||
pub(crate) fn pool(&self) -> ksp_onchain_transport_lib::HttpTransportPool {
|
||||
return self.pool.clone();
|
||||
}
|
||||
|
||||
/// Returns the composite-selected Transport profile identifier.
|
||||
#[must_use]
|
||||
pub(crate) fn profile_id(&self) -> &str {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -245,13 +245,13 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
|
||||
assert!(frontend.contains("backfill_options"));
|
||||
let html = read_text(root.join("frontend/main.html").as_path());
|
||||
assert!(html.contains("backfillHttpRoute"));
|
||||
for forbidden in ["backfill_start", "BackfillJobRuntime", "grpc", "websocket"] {
|
||||
for forbidden in ["grpc", "websocket"] {
|
||||
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
|
||||
fn pre_007_campaign_dto_and_backend_mapping_remain_the_authoritative_start_input() {
|
||||
let root = app_root();
|
||||
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
|
||||
@@ -291,8 +291,38 @@ fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
|
||||
assert!(frontend.contains("backfill_validate_request"));
|
||||
assert!(frontend.contains("Backfill Desk campaign validation requested"));
|
||||
assert!(frontend.contains("Aucun job n'a été démarré"));
|
||||
for forbidden in ["backfill_start", "BackfillJobRuntime", "BackfillJobHandle"] {
|
||||
assert!(!frontend.contains(forbidden), "pre.007 frontend opens deferred Start/runtime marker {forbidden}");
|
||||
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface opens deferred Start/runtime marker {forbidden}");
|
||||
}
|
||||
assert!(frontend.contains("backfill_validate_request"));
|
||||
assert!(tauri.contains("backfill_validate_request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_backend_owned() {
|
||||
let root = app_root();
|
||||
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||
let html = read_text(root.join("frontend/main.html").as_path());
|
||||
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
|
||||
let handle_index = state.find("let handle = runtime.handle();");
|
||||
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle)");
|
||||
assert!(handle_index.is_some());
|
||||
assert!(install_index.is_some());
|
||||
if let (std::option::Option::Some(handle_index), std::option::Option::Some(install_index)) = (handle_index, install_index) {
|
||||
assert!(handle_index < install_index);
|
||||
}
|
||||
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
|
||||
assert!(run.contains("std::sync::Mutex<std::option::Option<ActiveBackfillRun>>"));
|
||||
assert!(tauri.contains("backfill_start"));
|
||||
let prepare_index = tauri.find("state.prepare_backfill_start(request)");
|
||||
let spawn_index = tauri.find("tauri::async_runtime::spawn(async move");
|
||||
assert!(prepare_index.is_some());
|
||||
assert!(spawn_index.is_some());
|
||||
if let (std::option::Option::Some(prepare_index), std::option::Option::Some(spawn_index)) = (prepare_index, spawn_index) {
|
||||
assert!(prepare_index < spawn_index);
|
||||
}
|
||||
assert!(html.contains("startBackfillRequest"));
|
||||
assert!(html.contains("Démarrer le backfill"));
|
||||
assert!(frontend.contains("Backfill Desk campaign Start requested"));
|
||||
assert!(frontend.contains("backfill_start"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -91,7 +91,7 @@ fn pre_002_tauri_commands_remain_centralized() {
|
||||
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
|
||||
}
|
||||
}
|
||||
assert_eq!(command_count, 5);
|
||||
assert_eq!(command_count, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -204,3 +204,37 @@ fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_ow
|
||||
assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_start_surface_keeps_payload_and_physical_resources_backend_owned() {
|
||||
let root = app_root();
|
||||
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||
let response_start = dto.find("pub(crate) struct BackfillStartResponseDto");
|
||||
let preview_start = dto.find("pub(crate) struct BackfillRequestPreviewDto");
|
||||
assert!(response_start.is_some());
|
||||
assert!(preview_start.is_some());
|
||||
let response_source = match (response_start, preview_start) {
|
||||
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &dto[start..end],
|
||||
_ => "",
|
||||
};
|
||||
assert!(response_source.contains("pub(crate) job_id: String"));
|
||||
assert!(response_source.contains("pub(crate) state: String"));
|
||||
for forbidden in ["address", "signature", "provider", "endpoint", "url", "credential", "token", "network"] {
|
||||
assert!(!response_source.contains(forbidden), "Start acknowledgement leaks forbidden marker {forbidden}");
|
||||
}
|
||||
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||
assert!(run.contains("BackfillJobHandle"));
|
||||
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
|
||||
assert!(!run.contains("reqwest"));
|
||||
assert!(!run.contains("tokio_postgres"));
|
||||
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
let start_marker = frontend.find("Backfill Desk campaign Start requested");
|
||||
assert!(start_marker.is_some());
|
||||
let start_source = match start_marker {
|
||||
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 420, frontend.len())],
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
|
||||
assert!(!start_source.contains(forbidden), "Start tracing includes forbidden request payload marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
24
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal file
24
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
||||
let state = crate::BackfillRunState::new();
|
||||
let first = state.next_job_id();
|
||||
let second = state.next_job_id();
|
||||
assert!(first.is_ok());
|
||||
assert!(second.is_ok());
|
||||
let first = match first {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let second = match second {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_ne!(first, second);
|
||||
assert!(first.as_str().starts_with("backfill-desk-"));
|
||||
assert!(second.as_str().starts_with("backfill-desk-"));
|
||||
assert!(first.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
||||
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn request_limits_are_derived_from_job_owned_public_bounds() {
|
||||
@@ -29,3 +29,17 @@ fn request_options_expose_exact_current_commitments_and_http_scopes() {
|
||||
vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_acknowledgement_contains_only_backend_job_identity_and_initial_state() {
|
||||
let response = crate::BackfillStartResponseDto { job_id: "backfill-desk-1".to_owned(), state: "created".to_owned() };
|
||||
let serialized = serde_json::to_string(&response);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
assert!(serialized.contains("backfill-desk-1"));
|
||||
assert!(serialized.contains("created"));
|
||||
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token"] {
|
||||
assert!(!serialized.contains(forbidden));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user