v0.3.7-pre.011
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 434
|
# version: 435
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.7-pre.10"
|
version = "0.3.7-pre.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
|
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
@@ -138,6 +138,9 @@
|
|||||||
<button id="cancelBackfillRun" class="btn btn-outline-danger btn-sm" type="button" disabled>
|
<button id="cancelBackfillRun" class="btn btn-outline-danger btn-sm" type="button" disabled>
|
||||||
<i class="fa-solid fa-ban me-2" aria-hidden="true"></i>Annuler
|
<i class="fa-solid fa-ban me-2" aria-hidden="true"></i>Annuler
|
||||||
</button>
|
</button>
|
||||||
|
<button id="resumeBackfillRun" class="btn btn-outline-success btn-sm" type="button" disabled>
|
||||||
|
<i class="fa-solid fa-forward-step me-2" aria-hidden="true"></i>Reprendre
|
||||||
|
</button>
|
||||||
<button id="refreshBackfillStatus" class="btn btn-outline-primary btn-sm" type="button">
|
<button id="refreshBackfillStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Resynchroniser
|
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Resynchroniser
|
||||||
</button>
|
</button>
|
||||||
@@ -145,6 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
<div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
||||||
|
<div id="backfillResumeFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
|
||||||
<dl class="row mb-0 app-runtime-list">
|
<dl class="row mb-0 app-runtime-list">
|
||||||
<dt class="col-sm-5">Job</dt><dd id="runStatusJobId" class="col-sm-7 font-monospace">—</dd>
|
<dt class="col-sm-5">Job</dt><dd id="runStatusJobId" class="col-sm-7 font-monospace">—</dd>
|
||||||
<dt class="col-sm-5">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7">—</dd>
|
<dt class="col-sm-5">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7">—</dd>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import ResizeObserver from "resize-observer-polyfill";
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
@@ -9,6 +9,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
|||||||
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
|
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
|
||||||
import type { BackfillCancelResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts";
|
import type { BackfillCancelResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts";
|
||||||
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
|
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
|
||||||
|
import type { BackfillResumeResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts";
|
||||||
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.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 { 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 { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
|
||||||
@@ -42,7 +43,7 @@ const scopeLabels: Record<string, string> = {
|
|||||||
let compositionReadyForStart = false;
|
let compositionReadyForStart = false;
|
||||||
let currentRunStatus: BackfillRunStatusDto | null = null;
|
let currentRunStatus: BackfillRunStatusDto | null = null;
|
||||||
let runStartAccepted = false;
|
let runStartAccepted = false;
|
||||||
|
let resumeInFlight = false;
|
||||||
|
|
||||||
function isViewId(value: string | undefined): value is ViewId {
|
function isViewId(value: string | undefined): value is ViewId {
|
||||||
return value === "backfill" || value === "diagnostics";
|
return value === "backfill" || value === "diagnostics";
|
||||||
@@ -453,15 +454,20 @@ function bindBackfillRouteSelection(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "cancel" | "event" | "startup" | "start" | "user"): void {
|
function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "cancel" | "event" | "resume" | "startup" | "start" | "user"): void {
|
||||||
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
|
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
|
||||||
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
|
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
|
||||||
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
currentRunStatus = status;
|
currentRunStatus = status;
|
||||||
if (!status) {
|
if (!status) {
|
||||||
runStartAccepted = false;
|
runStartAccepted = false;
|
||||||
if (cancelButton) {
|
if (cancelButton) {
|
||||||
cancelButton.disabled = true;
|
cancelButton.disabled = true;
|
||||||
}
|
}
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = true;
|
||||||
|
}
|
||||||
|
resumeInFlight = false;
|
||||||
refreshStartButton();
|
refreshStartButton();
|
||||||
if (card) {
|
if (card) {
|
||||||
card.hidden = true;
|
card.hidden = true;
|
||||||
@@ -473,6 +479,12 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
|
|||||||
if (cancelButton) {
|
if (cancelButton) {
|
||||||
cancelButton.disabled = !status.active;
|
cancelButton.disabled = !status.active;
|
||||||
}
|
}
|
||||||
|
if (status.active) {
|
||||||
|
resumeInFlight = false;
|
||||||
|
}
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = !status.terminal || !status.checkpointPresent || resumeInFlight;
|
||||||
|
}
|
||||||
refreshStartButton();
|
refreshStartButton();
|
||||||
if (card) {
|
if (card) {
|
||||||
card.hidden = false;
|
card.hidden = false;
|
||||||
@@ -509,13 +521,46 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncBackfillStatus(source: "cancel" | "startup" | "start" | "user"): Promise<void> {
|
async function syncBackfillStatus(source: "cancel" | "resume" | "startup" | "start" | "user"): Promise<void> {
|
||||||
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
|
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
|
||||||
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
|
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
|
||||||
renderBackfillRunStatus(status, source);
|
renderBackfillRunStatus(status, source);
|
||||||
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
|
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resumeBackfillRun(): Promise<void> {
|
||||||
|
const status = currentRunStatus;
|
||||||
|
if (!status || !status.terminal || !status.checkpointPresent || resumeInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
|
resumeInFlight = true;
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.disabled = true;
|
||||||
|
}
|
||||||
|
frontendDebug("main", "Backfill Desk in-session Resume requested", { jobId: status.jobId, state: status.state });
|
||||||
|
try {
|
||||||
|
const response = await invokeKsp<BackfillResumeResponseDto>("main", "backfill_resume");
|
||||||
|
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.hidden = false;
|
||||||
|
feedback.textContent = `Checkpoint repris dans ${response.jobId} (${response.state}).`;
|
||||||
|
}
|
||||||
|
frontendInfo("main", "Backfill Desk in-session Resume accepted", {
|
||||||
|
accepted: response.accepted,
|
||||||
|
jobId: response.jobId,
|
||||||
|
state: response.state,
|
||||||
|
});
|
||||||
|
await syncBackfillStatus("resume");
|
||||||
|
} catch (_caughtError) {
|
||||||
|
resumeInFlight = false;
|
||||||
|
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
|
||||||
|
resumeButton.disabled = false;
|
||||||
|
}
|
||||||
|
frontendWarn("main", "Backfill Desk in-session Resume failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function bindBackfillStatusMonitoring(): Promise<void> {
|
async function bindBackfillStatusMonitoring(): Promise<void> {
|
||||||
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
|
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
|
||||||
renderBackfillRunStatus(event.payload, "event");
|
renderBackfillRunStatus(event.payload, "event");
|
||||||
@@ -560,7 +605,13 @@ async function bindBackfillStatusMonitoring(): Promise<void> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
frontendTrace("main", "Backfill Desk latest-value monitoring and cancellation handlers installed");
|
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
|
||||||
|
if (resumeButton) {
|
||||||
|
resumeButton.addEventListener("click", () => {
|
||||||
|
void resumeBackfillRun();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Backfill Desk latest-value monitoring, cancellation and Resume handlers installed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRuntimeStatus(status: ShellStatusDto): void {
|
function renderRuntimeStatus(status: ShellStatusDto): void {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ksp-app-backfill-desk",
|
"name": "ksp-app-backfill-desk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.7-pre.2",
|
"version": "0.3.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Shared backend state owned by the Backfill Desk Tauri application.
|
//! Shared backend state owned by the Backfill Desk Tauri application.
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ impl crate::AppState {
|
|||||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
config_document_count: document_count,
|
config_document_count: document_count,
|
||||||
fallback_logging_active: runtime.fallback_active,
|
fallback_logging_active: runtime.fallback_active,
|
||||||
shell_phase: "pre.010-cancel-races".to_owned(),
|
shell_phase: "pre.011-resume-checkpoint".to_owned(),
|
||||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -219,6 +219,7 @@ impl crate::AppState {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
let retained_request = mapped.clone();
|
||||||
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
|
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
|
||||||
let runtime = match runtime {
|
let runtime = match runtime {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -226,7 +227,7 @@ impl crate::AppState {
|
|||||||
};
|
};
|
||||||
let handle = runtime.handle();
|
let handle = runtime.handle();
|
||||||
let snapshots = handle.snapshots();
|
let snapshots = handle.snapshots();
|
||||||
let installed = self.backfill_runs.install(job_id.clone(), handle);
|
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
|
||||||
if let std::result::Result::Err(error) = installed {
|
if let std::result::Result::Err(error) = installed {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
@@ -264,6 +265,89 @@ impl crate::AppState {
|
|||||||
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
|
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepares one in-session Resume from the retained Rust-only checkpoint using a new backend Job identity.
|
||||||
|
pub(crate) fn prepare_backfill_resume(&self) -> 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 resume 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),
|
||||||
|
};
|
||||||
|
if !options.composition_ready {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk cannot resume without ready Transport and Store composition",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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 = self.backfill_runs.resume_request(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 store_network_matches = options.store_network.as_deref() == std::option::Option::Some(mapped.network().as_str());
|
||||||
|
let role_available = options.http_routes.iter().any(|route| return route.role == mapped.role().as_str());
|
||||||
|
if !store_network_matches || !role_available {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
|
||||||
|
"Backfill Desk retained Resume request no longer matches current Transport/Store composition",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let retained_request = mapped.clone();
|
||||||
|
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 snapshots = handle.snapshots();
|
||||||
|
let installed = self.backfill_runs.install(job_id.clone(), handle, retained_request);
|
||||||
|
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 resume 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 in-session Resume with a reissued Rust-only checkpoint"
|
||||||
|
);
|
||||||
|
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.
|
/// 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>> {
|
pub(crate) fn backfill_status(&self) -> ksp_core_lib::Result<std::option::Option<crate::BackfillRunStatusDto>> {
|
||||||
let notification = self.backfill_runs.current_notification();
|
let notification = self.backfill_runs.current_notification();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
|
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Single-active-run admission and launch ownership for Backfill Desk.
|
//! Single-active-run admission and launch ownership for Backfill Desk.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ impl crate::BackfillRunLaunch {
|
|||||||
/// Single-run control slot retained by the application while one Backfill runtime is active.
|
/// Single-run control slot retained by the application while one Backfill runtime is active.
|
||||||
pub(crate) struct BackfillRunState {
|
pub(crate) struct BackfillRunState {
|
||||||
active: std::sync::Mutex<std::option::Option<ActiveBackfillRun>>,
|
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>>>,
|
last_terminal: std::sync::Mutex<std::option::Option<TerminalBackfillRun>>,
|
||||||
next_sequence: std::sync::atomic::AtomicU64,
|
next_sequence: std::sync::atomic::AtomicU64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,12 @@ impl crate::BackfillRunState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Installs one control handle atomically and rejects concurrent Starts while another run owns the slot.
|
/// 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<()> {
|
pub(crate) fn install(
|
||||||
|
&self,
|
||||||
|
job_id: ksp_job_api::JobId,
|
||||||
|
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
let active = self.active.lock();
|
let active = self.active.lock();
|
||||||
let mut active = match active {
|
let mut active = match active {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -78,7 +83,7 @@ impl crate::BackfillRunState {
|
|||||||
"Backfill Desk already has an active Backfill run",
|
"Backfill Desk already has an active Backfill run",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id });
|
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id, request });
|
||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ impl crate::BackfillRunState {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let cancellation_requested = current.handle.is_cancellation_requested();
|
let cancellation_requested = current.handle.is_cancellation_requested();
|
||||||
|
let request = current.request.clone();
|
||||||
let source = current.handle.snapshots();
|
let source = current.handle.snapshots();
|
||||||
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||||
if notification.state().is_terminal() {
|
if notification.state().is_terminal() {
|
||||||
@@ -124,7 +130,7 @@ impl crate::BackfillRunState {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
*terminal = std::option::Option::Some(notification);
|
*terminal = std::option::Option::Some(TerminalBackfillRun { notification, request });
|
||||||
}
|
}
|
||||||
*active = std::option::Option::None;
|
*active = std::option::Option::None;
|
||||||
return std::result::Result::Ok(cancellation_requested);
|
return std::result::Result::Ok(cancellation_requested);
|
||||||
@@ -178,7 +184,8 @@ impl crate::BackfillRunState {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if let std::option::Option::Some(notification) = terminal.as_ref() {
|
if let std::option::Option::Some(terminal) = terminal.as_ref() {
|
||||||
|
let notification = &terminal.notification;
|
||||||
if notification.id().as_str() != job_id {
|
if notification.id().as_str() != job_id {
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
|
crate::ERROR_CODE_BACKFILL_RUN_MISMATCH,
|
||||||
@@ -245,7 +252,58 @@ impl crate::BackfillRunState {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return std::result::Result::Ok(terminal.clone());
|
return std::result::Result::Ok(terminal.as_ref().map(|terminal| return terminal.notification.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the last terminal request with its opaque checkpoint reissued onto one new Job identity.
|
||||||
|
pub(crate) fn resume_request(&self, job_id: ksp_job_api::JobId) -> ksp_core_lib::Result<ksp_job_backfill_lib::BackfillRequest> {
|
||||||
|
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 resume 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 cannot resume while another Backfill run owns the single-run slot",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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 resume state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let terminal = match terminal.as_ref() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
|
||||||
|
"Backfill Desk has no retained terminal run available for in-session Resume",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let checkpoint = terminal.notification.snapshot().checkpoint();
|
||||||
|
let checkpoint = match checkpoint {
|
||||||
|
std::option::Option::Some(value) => value.clone(),
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE,
|
||||||
|
"Backfill Desk retained terminal run has no safe checkpoint for in-session Resume",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return terminal.request.resume_for_job(job_id, checkpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
|
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
|
||||||
@@ -261,6 +319,13 @@ impl crate::BackfillRunState {
|
|||||||
struct ActiveBackfillRun {
|
struct ActiveBackfillRun {
|
||||||
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
handle: ksp_job_backfill_lib::BackfillJobHandle,
|
||||||
job_id: ksp_job_api::JobId,
|
job_id: ksp_job_api::JobId,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TerminalBackfillRun {
|
||||||
|
notification: ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
|
||||||
|
request: ksp_job_backfill_lib::BackfillRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
|
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
|
||||||
|
|
||||||
@@ -89,6 +89,19 @@ pub(crate) struct BackfillCancelResponseDto {
|
|||||||
pub(crate) state: String,
|
pub(crate) state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Safe acknowledgement returned when one retained checkpoint is resumed into a new backend Job lifecycle.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts")]
|
||||||
|
pub(crate) struct BackfillResumeResponseDto {
|
||||||
|
/// Whether the retained in-session checkpoint was accepted for a new run.
|
||||||
|
pub(crate) accepted: bool,
|
||||||
|
/// Newly allocated backend Job identifier owning the reissued checkpoint.
|
||||||
|
pub(crate) job_id: String,
|
||||||
|
/// Initial lifecycle state at the time Resume returns to the frontend.
|
||||||
|
pub(crate) state: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
|
/// 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
|
/// 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
|
// file: crates/ksp-app-backfill-desk/src/errors.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
|
//! 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");
|
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.
|
/// 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");
|
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
|
||||||
|
/// Backfill Desk has no retained safe checkpoint/request pair available for in-session Resume.
|
||||||
|
pub(crate) const ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_resume_unavailable");
|
||||||
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
/// 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");
|
pub(crate) const ERROR_CODE_BACKFILL_RUN_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_active");
|
||||||
/// Backfill Desk received a stale or mismatched Job identity for an active-run control operation.
|
/// Backfill Desk received a stale or mismatched Job identity for an active-run control operation.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
||||||
|
|
||||||
@@ -98,6 +98,8 @@ pub(crate) use self::dto_backfill::BackfillCancelResponseDto;
|
|||||||
pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
|
pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
|
||||||
/// Safe request preview produced after strict backend mapping.
|
/// Safe request preview produced after strict backend mapping.
|
||||||
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
|
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
|
||||||
|
/// Safe acknowledgement returned after in-session checkpoint Resume admission.
|
||||||
|
pub(crate) use self::dto_backfill::BackfillResumeResponseDto;
|
||||||
/// App-owned campaign request received from the frontend.
|
/// App-owned campaign request received from the frontend.
|
||||||
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
|
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
|
||||||
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
|
||||||
@@ -124,6 +126,8 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
|||||||
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
|
||||||
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
|
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
|
||||||
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
|
||||||
|
/// Backfill Desk has no retained safe checkpoint available for in-session Resume.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE;
|
||||||
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
|
/// 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;
|
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_ACTIVE;
|
||||||
/// Backfill Desk rejects stale or mismatched Job identity control requests.
|
/// Backfill Desk rejects stale or mismatched Job identity control requests.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
|||||||
return builder.invoke_handler(tauri::generate_handler![
|
return builder.invoke_handler(tauri::generate_handler![
|
||||||
backfill_cancel,
|
backfill_cancel,
|
||||||
backfill_options,
|
backfill_options,
|
||||||
|
backfill_resume,
|
||||||
backfill_start,
|
backfill_start,
|
||||||
backfill_status,
|
backfill_status,
|
||||||
backfill_validate_request,
|
backfill_validate_request,
|
||||||
@@ -206,6 +207,32 @@ fn backfill_start(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let response = launch.response();
|
let response = launch.response();
|
||||||
|
spawn_backfill_launch(app, launch);
|
||||||
|
return std::result::Result::Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn backfill_resume(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<crate::BackfillResumeResponseDto, crate::CommandErrorDto> {
|
||||||
|
let launch = state.prepare_backfill_resume();
|
||||||
|
let launch = match launch {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(project_command_error("backfill_resume", crate::TRACING_DOMAIN_RUN, &error));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let response = crate::BackfillResumeResponseDto {
|
||||||
|
accepted: true,
|
||||||
|
job_id: launch.job_id.as_str().to_owned(),
|
||||||
|
state: ksp_job_api::JobState::Created.code().to_owned(),
|
||||||
|
};
|
||||||
|
spawn_backfill_launch(app, launch);
|
||||||
|
return std::result::Result::Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_backfill_launch(app: tauri::AppHandle, launch: crate::BackfillRunLaunch) {
|
||||||
let monitor_app = app.clone();
|
let monitor_app = app.clone();
|
||||||
let monitor_source = launch.snapshots.clone();
|
let monitor_source = launch.snapshots.clone();
|
||||||
let _monitor_task = tauri::async_runtime::spawn(async move {
|
let _monitor_task = tauri::async_runtime::spawn(async move {
|
||||||
@@ -224,7 +251,7 @@ fn backfill_start(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return std::result::Result::Ok(response);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
|
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_
|
|||||||
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
|
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
|
||||||
let handle_index = state.find("let handle = runtime.handle();");
|
let handle_index = state.find("let handle = runtime.handle();");
|
||||||
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle)");
|
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle, retained_request)");
|
||||||
assert!(handle_index.is_some());
|
assert!(handle_index.is_some());
|
||||||
assert!(install_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) {
|
if let (std::option::Option::Some(handle_index), std::option::Option::Some(install_index)) = (handle_index, install_index) {
|
||||||
@@ -374,3 +374,33 @@ fn pre_010_cancel_is_targeted_idempotent_and_shutdown_requests_cooperative_cance
|
|||||||
assert!(frontend.contains("backfill_cancel"));
|
assert!(frontend.contains("backfill_cancel"));
|
||||||
assert!(frontend.contains("{ jobId: status.jobId }"));
|
assert!(frontend.contains("{ jobId: status.jobId }"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_011_resume_reissues_rust_only_checkpoint_for_new_backend_job_without_ipc_checkpoint_material() {
|
||||||
|
let root = app_root();
|
||||||
|
let workspace = root.join("../..");
|
||||||
|
let checkpoint = read_text(workspace.join("crates/ksp-job-backfill-lib/src/checkpoint.rs").as_path());
|
||||||
|
assert!(checkpoint.contains("reissue_for_job"));
|
||||||
|
let request = read_text(workspace.join("crates/ksp-job-backfill-lib/src/request.rs").as_path());
|
||||||
|
assert!(request.contains("pub fn resume_for_job"));
|
||||||
|
assert!(request.contains("validate_request_checkpoint"));
|
||||||
|
assert!(request.contains("checkpoint.reissue_for_job(job_id.clone())"));
|
||||||
|
let run = read_text(root.join("src/backfill_run.rs").as_path());
|
||||||
|
for required in ["TerminalBackfillRun", "request: ksp_job_backfill_lib::BackfillRequest", "snapshot().checkpoint()", "resume_for_job(job_id, checkpoint)"] {
|
||||||
|
assert!(run.contains(required), "missing Rust-only Resume ownership marker {required}");
|
||||||
|
}
|
||||||
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||||
|
for required in ["prepare_backfill_resume", "options.composition_ready", "store_network_matches", "role_available"] {
|
||||||
|
assert!(state.contains(required), "missing Resume admission marker {required}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("backfill_resume"));
|
||||||
|
assert!(tauri.contains("spawn_backfill_launch"));
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(frontend.contains("resumeBackfillRun"));
|
||||||
|
assert!(frontend.contains("checkpointPresent"));
|
||||||
|
assert!(frontend.contains("backfill_resume"));
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
assert!(html.contains("resumeBackfillRun"));
|
||||||
|
assert!(html.contains("Reprendre"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||||
// version: 13
|
// version: 14
|
||||||
|
|
||||||
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
|
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ fn pre_002_tauri_commands_remain_centralized() {
|
|||||||
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
|
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(command_count, 8);
|
assert_eq!(command_count, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -295,3 +295,47 @@ fn pre_010_cancel_surface_targets_backend_job_identity_without_business_payloads
|
|||||||
assert!(!cancel_source.contains(forbidden), "Cancel tracing includes forbidden campaign payload marker {forbidden}");
|
assert!(!cancel_source.contains(forbidden), "Cancel tracing includes forbidden campaign payload marker {forbidden}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_011_resume_surface_exposes_only_new_job_acknowledgement_and_never_checkpoint_payload() {
|
||||||
|
let root = app_root();
|
||||||
|
let dto_source = read_text(root.join("src/dto_backfill.rs").as_path());
|
||||||
|
let dto = struct_source(dto_source.as_str(), "BackfillResumeResponseDto");
|
||||||
|
for required in ["accepted", "job_id", "state"] {
|
||||||
|
assert!(dto.contains(required), "Resume acknowledgement missing safe field {required}");
|
||||||
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"pub(crate) address:",
|
||||||
|
"pub(crate) signature:",
|
||||||
|
"pub(crate) endpoint:",
|
||||||
|
"pub(crate) provider:",
|
||||||
|
"pub(crate) credential:",
|
||||||
|
"pub(crate) token:",
|
||||||
|
"pub(crate) checkpoint:",
|
||||||
|
"pub(crate) payload:",
|
||||||
|
"pub(crate) network:",
|
||||||
|
] {
|
||||||
|
assert!(!dto.contains(forbidden), "Resume acknowledgement leaks forbidden field marker {forbidden}");
|
||||||
|
}
|
||||||
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||||
|
let resume_marker = tauri.find("fn backfill_resume");
|
||||||
|
assert!(resume_marker.is_some());
|
||||||
|
let resume_source = match resume_marker {
|
||||||
|
std::option::Option::Some(index) => &tauri[index..std::cmp::min(index + 1300, tauri.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
for forbidden in ["BackfillCheckpoint", "checkpoint:", "address:", "signature:", "provider:", "endpoint:"] {
|
||||||
|
assert!(!resume_source.contains(forbidden), "Resume IPC command leaks forbidden marker {forbidden}");
|
||||||
|
}
|
||||||
|
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
let resume_marker = frontend.find("Backfill Desk in-session Resume requested");
|
||||||
|
assert!(resume_marker.is_some());
|
||||||
|
let resume_source = match resume_marker {
|
||||||
|
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 1100, frontend.len())],
|
||||||
|
std::option::Option::None => "",
|
||||||
|
};
|
||||||
|
assert!(resume_source.contains("backfill_resume"));
|
||||||
|
for forbidden in ["checkpoint:", "address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
|
||||||
|
assert!(!resume_source.contains(forbidden), "Resume frontend sends or logs forbidden payload marker {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
||||||
@@ -23,7 +23,9 @@ fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
|
|||||||
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancellable_runtime(job_id: &str) -> std::option::Option<(ksp_job_api::JobId, ksp_job_backfill_lib::BackfillJobRuntime)> {
|
fn cancellable_runtime(
|
||||||
|
job_id: &str,
|
||||||
|
) -> std::option::Option<(ksp_job_api::JobId, ksp_job_backfill_lib::BackfillRequest, ksp_job_backfill_lib::BackfillJobRuntime)> {
|
||||||
let job_id = match ksp_job_api::JobId::new(job_id) {
|
let job_id = match ksp_job_api::JobId::new(job_id) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
@@ -48,11 +50,12 @@ fn cancellable_runtime(job_id: &str) -> std::option::Option<(ksp_job_api::JobId,
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
|
let retained_request = request.clone();
|
||||||
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
|
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::option::Option::None,
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
};
|
};
|
||||||
return std::option::Option::Some((job_id, runtime));
|
return std::option::Option::Some((job_id, retained_request, runtime));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -60,11 +63,11 @@ fn targeted_cancel_is_idempotent_and_rejects_stale_job_identity() {
|
|||||||
let state = crate::BackfillRunState::new();
|
let state = crate::BackfillRunState::new();
|
||||||
let runtime = cancellable_runtime("backfill-desk-41");
|
let runtime = cancellable_runtime("backfill-desk-41");
|
||||||
assert!(runtime.is_some());
|
assert!(runtime.is_some());
|
||||||
let (job_id, runtime) = match runtime {
|
let (job_id, request, runtime) = match runtime {
|
||||||
std::option::Option::Some(value) => value,
|
std::option::Option::Some(value) => value,
|
||||||
std::option::Option::None => return,
|
std::option::Option::None => return,
|
||||||
};
|
};
|
||||||
let installed = state.install(job_id.clone(), runtime.handle());
|
let installed = state.install(job_id.clone(), runtime.handle(), request);
|
||||||
assert!(installed.is_ok());
|
assert!(installed.is_ok());
|
||||||
let first = state.cancel(job_id.as_str());
|
let first = state.cancel(job_id.as_str());
|
||||||
assert!(first.is_ok());
|
assert!(first.is_ok());
|
||||||
@@ -85,3 +88,30 @@ fn targeted_cancel_is_idempotent_and_rejects_stale_job_identity() {
|
|||||||
assert_eq!(error.code().code(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.code());
|
assert_eq!(error.code().code(), crate::ERROR_CODE_BACKFILL_RUN_MISMATCH.code());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_requires_terminal_checkpoint_and_rejects_active_run() {
|
||||||
|
let state = crate::BackfillRunState::new();
|
||||||
|
let next = match ksp_job_api::JobId::new("backfill-desk-50") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let unavailable = state.resume_request(next.clone());
|
||||||
|
assert!(unavailable.is_err());
|
||||||
|
if let std::result::Result::Err(error) = unavailable {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RESUME_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
let runtime = cancellable_runtime("backfill-desk-49");
|
||||||
|
assert!(runtime.is_some());
|
||||||
|
let (job_id, request, runtime) = match runtime {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let installed = state.install(job_id, runtime.handle(), request);
|
||||||
|
assert!(installed.is_ok());
|
||||||
|
let active = state.resume_request(next);
|
||||||
|
assert!(active.is_err());
|
||||||
|
if let std::result::Result::Err(error) = active {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RUN_ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_limits_are_derived_from_job_owned_public_bounds() {
|
fn request_limits_are_derived_from_job_owned_public_bounds() {
|
||||||
@@ -58,3 +58,18 @@ fn cancel_acknowledgement_contains_only_backend_job_identity_acceptance_and_stat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_acknowledgement_contains_only_new_backend_job_identity_acceptance_and_state() {
|
||||||
|
let response = crate::BackfillResumeResponseDto { accepted: true, job_id: "backfill-desk-2".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-2"));
|
||||||
|
assert!(serialized.contains("created"));
|
||||||
|
assert!(serialized.contains("accepted"));
|
||||||
|
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token", "checkpoint", "payload"] {
|
||||||
|
assert!(!serialized.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/src/checkpoint.rs
|
// file: crates/ksp-job-backfill-lib/src/checkpoint.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Opaque caller-owned checkpoint for one controlled Backfill resumption.
|
/// Opaque caller-owned checkpoint for one controlled Backfill resumption.
|
||||||
///
|
///
|
||||||
@@ -43,6 +43,12 @@ impl crate::BackfillCheckpoint {
|
|||||||
return Self { job_id, scope_fingerprint, completed_prefix, resume_before };
|
return Self { job_id, scope_fingerprint, completed_prefix, resume_before };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reissues this opaque frontier for a new caller-owned Job identity without changing scope or progress.
|
||||||
|
pub(crate) fn reissue_for_job(mut self, job_id: ksp_job_api::JobId) -> Self {
|
||||||
|
self.job_id = job_id;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the internal exclusive `before` cursor used only by controlled Before resumption.
|
/// Returns the internal exclusive `before` cursor used only by controlled Before resumption.
|
||||||
pub(crate) const fn resume_before(&self) -> std::option::Option<&crate::BackfillSignature> {
|
pub(crate) const fn resume_before(&self) -> std::option::Option<&crate::BackfillSignature> {
|
||||||
return self.resume_before.as_ref();
|
return self.resume_before.as_ref();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/src/request.rs
|
// file: crates/ksp-job-backfill-lib/src/request.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
use sha2::Digest; // rust-rules: trait-import
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
@@ -396,6 +396,19 @@ impl crate::BackfillRequest {
|
|||||||
return std::result::Result::Ok(self);
|
return std::result::Result::Ok(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reissues a validated checkpoint onto a new Job lifecycle while preserving exact request semantics.
|
||||||
|
pub fn resume_for_job(&self, job_id: ksp_job_api::JobId, checkpoint: crate::BackfillCheckpoint) -> ksp_core_lib::Result<Self> {
|
||||||
|
let validation = crate::validate_request_checkpoint(&self.job_id, self.scope_fingerprint, self.scope.kind(), &checkpoint);
|
||||||
|
if let std::result::Result::Err(error) = validation {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let checkpoint = checkpoint.reissue_for_job(job_id.clone());
|
||||||
|
let mut resumed = self.clone();
|
||||||
|
resumed.job_id = job_id;
|
||||||
|
resumed.checkpoint = std::option::Option::None;
|
||||||
|
return resumed.with_checkpoint(checkpoint);
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the validated optional checkpoint supplied for controlled resumption.
|
/// Returns the validated optional checkpoint supplied for controlled resumption.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn checkpoint(&self) -> std::option::Option<&crate::BackfillCheckpoint> {
|
pub const fn checkpoint(&self) -> std::option::Option<&crate::BackfillCheckpoint> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/unit_tests/request.rs
|
// file: crates/ksp-job-backfill-lib/unit_tests/request.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
fn signature(character: char) -> std::option::Option<crate::BackfillSignature> {
|
fn signature(character: char) -> std::option::Option<crate::BackfillSignature> {
|
||||||
return match crate::BackfillSignature::new(character.to_string().repeat(crate::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
return match crate::BackfillSignature::new(character.to_string().repeat(crate::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||||
@@ -232,3 +232,71 @@ fn pre_005_scope_kind_and_anchor_are_distinct_semantics() {
|
|||||||
assert_ne!(before, after);
|
assert_ne!(before, after);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_011_resume_reissues_checkpoint_for_new_job_without_changing_scope_semantics() {
|
||||||
|
let network = match network("mainnet-beta") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let original_job = match job_id("backfill:resume-original") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let resumed_job = match job_id("backfill:resume-next") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let request = crate::BackfillRequest::new(
|
||||||
|
original_job.clone(),
|
||||||
|
network,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
|
||||||
|
crate::BackfillCommitment::Finalized,
|
||||||
|
crate::BackfillScope::before_address(
|
||||||
|
ksp_core_lib::Pubkey::new_from_array([11_u8; 32]),
|
||||||
|
match signature('8') {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
100,
|
||||||
|
10,
|
||||||
|
1000,
|
||||||
|
4,
|
||||||
|
std::option::Option::Some(42),
|
||||||
|
);
|
||||||
|
let request = match request {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let resume_before = match signature('9') {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let checkpoint = crate::BackfillCheckpoint::new(original_job, request.scope_fingerprint(), 7, std::option::Option::Some(resume_before.clone()));
|
||||||
|
let resumed = request.resume_for_job(resumed_job.clone(), checkpoint);
|
||||||
|
assert!(resumed.is_ok());
|
||||||
|
let resumed = match resumed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resumed.job_id(), &resumed_job);
|
||||||
|
assert_eq!(resumed.scope_fingerprint(), request.scope_fingerprint());
|
||||||
|
assert_eq!(resumed.network(), request.network());
|
||||||
|
assert_eq!(resumed.role(), request.role());
|
||||||
|
assert_eq!(resumed.commitment(), request.commitment());
|
||||||
|
assert_eq!(resumed.scope(), request.scope());
|
||||||
|
assert_eq!(resumed.page_size(), request.page_size());
|
||||||
|
assert_eq!(resumed.max_pages(), request.max_pages());
|
||||||
|
assert_eq!(resumed.max_candidates(), request.max_candidates());
|
||||||
|
assert_eq!(resumed.hydration_concurrency(), request.hydration_concurrency());
|
||||||
|
assert_eq!(resumed.min_context_slot(), request.min_context_slot());
|
||||||
|
let checkpoint = resumed.checkpoint();
|
||||||
|
assert!(checkpoint.is_some());
|
||||||
|
if let std::option::Option::Some(checkpoint) = checkpoint {
|
||||||
|
assert_eq!(checkpoint.job_id(), &resumed_job);
|
||||||
|
assert_eq!(checkpoint.scope_fingerprint(), request.scope_fingerprint());
|
||||||
|
assert_eq!(checkpoint.completed_prefix(), 7);
|
||||||
|
assert_eq!(checkpoint.resume_before(), std::option::Option::Some(&resume_before));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
153
deltas/0.3.7/pre.011.md
Normal file
153
deltas/0.3.7/pre.011.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<!-- file: deltas/0.3.7/pre.011.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.7-pre.011` — checkpoint/frontier et Resume in-session
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.7-pre.010
|
||||||
|
workspace.package.version = 0.3.7-pre.10
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Objectif
|
||||||
|
|
||||||
|
Ouvrir Resume sans introduire de checkpoint durable ni affaiblir l'identité du checkpoint : conserver la requête terminale et son checkpoint uniquement dans Rust, réémettre le checkpoint vers un nouveau JobId backend après validation de son identité d'origine, puis relancer le même runtime/monitoring que Start.
|
||||||
|
|
||||||
|
## 3. Reprise contrôlée côté Job Backfill
|
||||||
|
|
||||||
|
`BackfillCheckpoint` reste opaque et lié au JobId qui l'a produit. `ksp-job-backfill-lib` ajoute `BackfillRequest::resume_for_job` :
|
||||||
|
|
||||||
|
- le checkpoint est validé contre le JobId et le scope fingerprint de la requête terminale d'origine ;
|
||||||
|
- seul le JobId est réémis pour le nouveau lifecycle ;
|
||||||
|
- scope fingerprint, completed prefix et éventuel cursor `before` restent inchangés ;
|
||||||
|
- `BackfillRequest::with_checkpoint` revalide ensuite le checkpoint réémis contre la nouvelle requête.
|
||||||
|
|
||||||
|
Aucun constructeur public de checkpoint, aucune sérialisation et aucun cursor exposé ne sont ajoutés.
|
||||||
|
|
||||||
|
## 4. Ownership Rust-only côté Desk
|
||||||
|
|
||||||
|
`BackfillRunState` retient désormais avec le dernier terminal la `BackfillRequest` exacte qui a produit ce terminal. Cette requête reste exclusivement backend et n'est jamais projetée vers IPC.
|
||||||
|
|
||||||
|
`resume_request(new_job_id)` :
|
||||||
|
|
||||||
|
- refuse Resume lorsqu'un run actif possède le slot ;
|
||||||
|
- refuse l'absence de terminal ou de checkpoint ;
|
||||||
|
- extrait le checkpoint uniquement depuis le snapshot terminal retenu ;
|
||||||
|
- appelle `BackfillRequest::resume_for_job` avec le nouveau JobId backend.
|
||||||
|
|
||||||
|
## 5. Admission Resume
|
||||||
|
|
||||||
|
`AppState::prepare_backfill_resume` :
|
||||||
|
|
||||||
|
- refuse le shutdown en cours ;
|
||||||
|
- exige `composition_ready` ;
|
||||||
|
- alloue un nouveau JobId backend ;
|
||||||
|
- reconstruit la requête via le checkpoint Rust-only ;
|
||||||
|
- revalide réseau Store et rôle HTTP contre la composition courante ;
|
||||||
|
- construit `BackfillJobRuntime`, installe le handle single-run, acquiert Transport/Store puis retourne `BackfillRunLaunch`.
|
||||||
|
|
||||||
|
Start et Resume partagent ensuite `spawn_backfill_launch`, donc le monitoring latest-value et la restitution du Store restent identiques.
|
||||||
|
|
||||||
|
## 6. IPC et frontend
|
||||||
|
|
||||||
|
Nouvelle commande :
|
||||||
|
|
||||||
|
```text
|
||||||
|
backfill_resume() -> BackfillResumeResponseDto
|
||||||
|
```
|
||||||
|
|
||||||
|
`BackfillResumeResponseDto` contient uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
accepted
|
||||||
|
job_id
|
||||||
|
state
|
||||||
|
```
|
||||||
|
|
||||||
|
Le frontend expose **Reprendre** seulement pour un snapshot terminal avec `checkpoint_present=true`. La commande Resume n'envoie aucun argument métier. Aucun checkpoint, adresse, signature, ancre, provider, endpoint, credential, network ou payload RAW ne traverse IPC.
|
||||||
|
|
||||||
|
## 7. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.7-pre.11
|
||||||
|
label = 0.3.7-pre.011
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.7/pre.011.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-app-backfill-desk/frontend/main.html
|
||||||
|
crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||||
|
crates/ksp-app-backfill-desk/src/app_state.rs
|
||||||
|
crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||||
|
crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
||||||
|
crates/ksp-app-backfill-desk/src/errors.rs
|
||||||
|
crates/ksp-app-backfill-desk/src/lib.rs
|
||||||
|
crates/ksp-app-backfill-desk/src/tauri.rs
|
||||||
|
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||||
|
crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||||
|
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||||
|
crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
||||||
|
crates/ksp-job-backfill-lib/src/checkpoint.rs
|
||||||
|
crates/ksp-job-backfill-lib/src/request.rs
|
||||||
|
crates/ksp-job-backfill-lib/unit_tests/request.rs
|
||||||
|
docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
|
||||||
|
docs/validation/024-V0_3_7_BACKFILL_DESK.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## 11. Gate précédent acquis
|
||||||
|
|
||||||
|
Le replay opérateur de `0.3.7-pre.010` est intégralement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, 49 tests unitaires Job Backfill et toutes les suites Backfill Desk passent. Le smoke Tauri Mainnet confirme aussi le Cancel ciblé, le passage en `cancelling`, la terminalisation puis la libération du slot avant shutdown.
|
||||||
|
|
||||||
|
## 12. Validations exécutées dans l'environnement d'assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrôles ciblés :
|
||||||
|
|
||||||
|
- `resume_for_job` valide le checkpoint d'origine puis réémet uniquement le JobId ;
|
||||||
|
- scope fingerprint, frontier et cursor interne restent inchangés ;
|
||||||
|
- requête terminale/checkpoint restent Rust-only ;
|
||||||
|
- Resume refuse run actif ou checkpoint absent ;
|
||||||
|
- réseau Store + rôle HTTP revalidés avant admission ;
|
||||||
|
- DTO Resume limité aux métadonnées sûres ;
|
||||||
|
- frontend Resume sans payload de campagne/checkpoint ;
|
||||||
|
- `std.store.json` inchangé par le delta ;
|
||||||
|
- aucune dépendance/feature Cargo modifiée ;
|
||||||
|
- syntaxe TypeScript par transpilation locale ;
|
||||||
|
- contrôle différentiel des headers et reconstruction stricte par application du delta.
|
||||||
|
|
||||||
|
## 13. Validations non exécutées dans l'environnement d'assemblage
|
||||||
|
|
||||||
|
`cargo` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Gate opérateur obligatoire avant `pre.012` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo test -p ksp-app-backfill-desk
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo tree` n'est requis pour cette tranche : dépendances et features Cargo sont inchangées.
|
||||||
|
|
||||||
|
## 14. Suite
|
||||||
|
|
||||||
|
`pre.012` terminera le frontend fonctionnel/polish, notamment l'autocomplete libre de Program IDs alimenté depuis `ksp-core-lib::entries()`/`ProgramIdEntry`, sans liste hardcodée ni checkpoint durable.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
|
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Plan v0.3.7 — Backfill Desk
|
# Plan v0.3.7 — Backfill Desk
|
||||||
|
|
||||||
@@ -192,9 +192,11 @@ Règles :
|
|||||||
|
|
||||||
## 10. Reprise et checkpoint
|
## 10. Reprise et checkpoint
|
||||||
|
|
||||||
V1 conserve le dernier `BackfillCheckpoint` uniquement dans Rust. L'UI voit `checkpoint_present` et `contiguous_completed`, jamais les octets/identité internes du checkpoint.
|
V1 conserve le dernier `BackfillCheckpoint` et la requête sémantique terminale uniquement dans Rust. L'UI voit `checkpoint_present` et `contiguous_completed`, jamais les octets/identité internes du checkpoint.
|
||||||
|
|
||||||
`Resume` reconstruit une requête sémantiquement identique avec un nouveau lifecycle contrôlé et réattache le checkpoint via `BackfillRequest::with_checkpoint`. Le backend doit préserver les éléments participant au scope fingerprint ; toute modification de scope/commitment/bornes sémantiques force une nouvelle campagne et invalide Resume.
|
Le checkpoint concret reste lié au JobId qui l'a produit. Pour créer un nouveau lifecycle sans affaiblir cette garantie, `ksp-job-backfill-lib` valide d'abord le checkpoint contre la requête terminale d'origine puis `BackfillRequest::resume_for_job` le réémet sur le nouveau JobId backend en conservant exactement scope fingerprint, frontier et cursor interne. La Desk ne fabrique ni ne modifie ces éléments.
|
||||||
|
|
||||||
|
`Resume` exige en outre que la composition Transport/Store courante reste compatible avec le réseau et le rôle logique de la requête retenue. Toute modification de scope/commitment/bornes sémantiques appartient à une nouvelle campagne et n'utilise pas Resume.
|
||||||
|
|
||||||
Aucun JSON, fichier ou table checkpoint n'est inventé. La reprise après crash/redémarrage est reportée à une évolution explicite du contrat Backfill si un besoin durable est démontré.
|
Aucun JSON, fichier ou table checkpoint n'est inventé. La reprise après crash/redémarrage est reportée à une évolution explicite du contrat Backfill si un besoin durable est démontré.
|
||||||
|
|
||||||
@@ -282,7 +284,9 @@ Le frontend expose **Annuler** uniquement pour un status actif, trace seulement
|
|||||||
|
|
||||||
### pre.011 — checkpoint/frontier et Resume
|
### pre.011 — checkpoint/frontier et Resume
|
||||||
|
|
||||||
Projection sûre, conservation Rust-only du checkpoint, reprise in-session et invariants par scope.
|
Conserver avec le dernier terminal la requête KSP ayant produit le checkpoint, uniquement dans l'état Rust de la Desk. `ksp-job-backfill-lib` ajoute `BackfillRequest::resume_for_job` : l'opération valide le checkpoint contre son JobId et son scope fingerprint d'origine, puis réémet uniquement l'identité Job pour le nouveau lifecycle sans exposer ni modifier cursor/frontier.
|
||||||
|
|
||||||
|
La commande `backfill_resume()` alloue un nouveau JobId backend, refuse un run actif ou l'absence de checkpoint, revalide la compatibilité réseau/rôle avec la composition courante, reconstruit `BackfillJobRuntime` et réutilise le même chemin de spawn/monitoring que Start. Le frontend expose **Reprendre** seulement sur un terminal avec `checkpoint_present`. Aucun checkpoint, adresse, signature ou paramètre de campagne ne traverse cette commande IPC.
|
||||||
|
|
||||||
### pre.012 — frontend fonctionnel et polish
|
### pre.012 — frontend fonctionnel et polish
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
|
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
|
||||||
<!-- version: 18 -->
|
<!-- version: 19 -->
|
||||||
|
|
||||||
# Validation v0.3.7 — Backfill Desk
|
# Validation v0.3.7 — Backfill Desk
|
||||||
|
|
||||||
@@ -500,4 +500,30 @@ Le frontend ajoute un bouton **Annuler** au monitoring, transmet uniquement le J
|
|||||||
- [X] frontend Cancel sans adresse/signature/provider/endpoint/checkpoint/payload ;
|
- [X] frontend Cancel sans adresse/signature/provider/endpoint/checkpoint/payload ;
|
||||||
- [X] aucune dépendance/feature Cargo ajoutée ;
|
- [X] aucune dépendance/feature Cargo ajoutée ;
|
||||||
- [X] Resume non ouvert ;
|
- [X] Resume non ouvert ;
|
||||||
- [ ] `cargo fmt/check/clippy/test` de `pre.010` à rejouer par l'opérateur.
|
- [X] replay opérateur `cargo fmt/check/clippy/test` de `pre.010` propre ; smoke Tauri Mainnet confirme Cancel ciblé, terminalisation puis shutdown Store.
|
||||||
|
|
||||||
|
## 25. `pre.011` — checkpoint/frontier et Resume in-session
|
||||||
|
|
||||||
|
Le replay opérateur de `pre.010` ferme le couloir Cancel : audits Rust/Markdown, `cargo check --workspace`, Clippy, les 49 tests unitaires `ksp-job-backfill-lib` et toutes les suites Backfill Desk passent. Le smoke Tauri Mainnet confirme également le chemin réel : Store ready, campagne HTTP active, Cancel ciblé accepté, transition `cancelling`, terminalisation du Job puis libération du slot avant shutdown. La phase `finished` observée est la phase terminale commune ; le lifecycle Job reste `cancelled`.
|
||||||
|
|
||||||
|
`pre.011` ouvre la reprise in-session sans sérialisation du checkpoint. Le contrat existant lie volontairement `BackfillCheckpoint` au JobId qui l'a produit ; la Desk ne peut donc pas simplement attacher ce checkpoint à un nouveau Job. `ksp-job-backfill-lib` ajoute `BackfillRequest::resume_for_job` : le checkpoint est d'abord validé contre la requête d'origine puis réémis en interne pour le nouveau JobId, avec scope fingerprint, completed prefix et éventuel cursor Before inchangés.
|
||||||
|
|
||||||
|
`BackfillRunState` retient maintenant la requête terminale avec la notification terminale, uniquement en mémoire Rust. `backfill_resume` n'accepte aucun argument frontend, alloue un nouveau JobId backend, exige un checkpoint présent, refuse tout run actif et revalide que réseau Store et rôle HTTP correspondent encore à la composition courante. La commande utilise le même chemin `spawn_backfill_launch` et le même monitoring latest-value que Start.
|
||||||
|
|
||||||
|
Le frontend expose **Reprendre** uniquement pour un snapshot terminal avec `checkpoint_present=true`. `BackfillResumeResponseDto` contient uniquement `accepted`, `job_id` et `state`; aucun checkpoint, cursor, adresse, signature, provider, endpoint ou payload RAW ne traverse IPC.
|
||||||
|
|
||||||
|
### Gate statique local `pre.011`
|
||||||
|
|
||||||
|
- [X] checkpoint original validé avant réémission vers un nouveau JobId ;
|
||||||
|
- [X] scope fingerprint/frontier/cursor inchangés par `resume_for_job` ;
|
||||||
|
- [X] requête terminale + checkpoint retenus uniquement dans `BackfillRunState` Rust ;
|
||||||
|
- [X] Resume refusé sans terminal/checkpoint ou lorsqu'un run est déjà actif ;
|
||||||
|
- [X] compatibilité réseau Store + rôle HTTP revalidée avant reprise ;
|
||||||
|
- [X] nouveau JobId exclusivement backend ;
|
||||||
|
- [X] chemin de spawn/monitoring partagé avec Start ;
|
||||||
|
- [X] DTO Resume limité à `accepted`, `job_id`, `state` ;
|
||||||
|
- [X] frontend Resume sans checkpoint ni payload de campagne ;
|
||||||
|
- [X] aucun fichier/table/JSON de checkpoint durable ajouté ;
|
||||||
|
- [X] aucune dépendance/feature Cargo ajoutée ;
|
||||||
|
- [ ] `cargo fmt/check/clippy/test` de `pre.011` à rejouer par l'opérateur.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user