0.3.15-pre.012
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 621
|
# version: 622
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-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-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-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-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.15-pre.11.fix.1"
|
version = "0.3.15-pre.12"
|
||||||
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-raw-transaction-ingest-desk/README.md -->
|
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
|
||||||
<!-- version: 13 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# `ksp-app-raw-transaction-ingest-desk`
|
# `ksp-app-raw-transaction-ingest-desk`
|
||||||
|
|
||||||
@@ -31,11 +31,15 @@ Au Start, le backend revalide une sélection logique par `profile_id + route_id
|
|||||||
|
|
||||||
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
|
Le runtime accepte désormais plusieurs routes simultanées sur un même réseau. Chaque route possède son Worker indépendant ; deux Starts de la même identité logique `profile_id + route_id` sont refusés, et un Start d'un autre réseau est refusé tant que le Store partagé reste ouvert. Le premier Worker ouvre et valide le Store, les suivants réutilisent le même `Arc<Store>`, puis la dernière route terminale déclenche seule la fermeture explicite du Store. Un fault d'une route ne stoppe pas les Workers siblings. Stop est ciblé par route et reste idempotent lorsqu'un terminal sûr a déjà été retenu.
|
||||||
|
|
||||||
|
Les races de contrôle sont fermées côté backend : un Stop qui rencontre une route encore `Starting` marque cette réservation pour arrêt coopératif et attend son cleanup au lieu de la considérer inactive. Dès qu'une fermeture de l'application commence, tout nouveau Start est refusé, les routes actives reçoivent Stop et les Starts déjà réservés sont marqués avant activation. La fenêtre principale ne quitte le processus qu'après cleanup borné des Workers et du Store partagé, ou avec un code d'échec après expiration de la borne.
|
||||||
|
|
||||||
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
|
Les profils applicatifs sont réseau-centriques (`devnet`, `mainnet`, `testnet`). Un profil peut agréger plusieurs profils Transport du même réseau : le provider reste une source de capability et ne devient jamais une identité réseau ni un choix de profil applicatif.
|
||||||
Une route explicitement liée à un provider n’est projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais n’est pas présentée sur Testnet tant qu’Helius ne fournit pas de source Testnet configurée.
|
Une route explicitement liée à un provider n’est projetée pour un réseau que si le composite déclare une source Transport de ce provider. Ainsi, la route Helius Transaction existe sur Devnet/Mainnet mais n’est pas présentée sur Testnet tant qu’Helius ne fournit pas de source Testnet configurée.
|
||||||
|
|
||||||
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
|
Le frontend ne reçoit aucune URL, credential, URI Store, metadata secrète, source key, handle runtime ou payload `RawTransaction`.
|
||||||
|
|
||||||
|
Les requêtes IPC Start/Stop sont shape-strict : les champs inconnus sont rejetés et `profile_id` est borné/validé avant toute reconstruction. Le bridge de logging frontend applique également des bornes strictes et refuse les caractères de contrôle avant émission dans la façade Logging.
|
||||||
|
|
||||||
## Routes V1
|
## Routes V1
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
|
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
|
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
|
||||||
|
|
||||||
@@ -36,12 +36,16 @@ Une route provider-specific dont aucune source n’est déclarée pour le résea
|
|||||||
|
|
||||||
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
|
Les URLs, tokens, metadata secrètes, handles Transport/Store/Worker, URI Store, source keys et payloads RAW restent backend-only.
|
||||||
|
|
||||||
|
Les commandes Start/Stop refusent les champs IPC inconnus et les identifiants logiques non bornés ou contenant des caractères de contrôle. Le logging frontend est lui aussi borné avant l'IPC ; une entrée hostile ne doit jamais être recopiée dans les diagnostics backend.
|
||||||
|
|
||||||
### Start/Stop multi-route
|
### Start/Stop multi-route
|
||||||
|
|
||||||
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
|
Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `Start`. Le backend revalide la génération d'inventaire et la composition Config, ouvre le Store correspondant lorsqu'aucun Store compatible n'est encore actif, exige son état `Ready`, construit le Worker exact puis installe son handle avant de renvoyer l'accusé runtime.
|
||||||
|
|
||||||
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
|
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
|
||||||
|
|
||||||
|
Un Stop peut être demandé même si le Start ciblé n'a pas encore fini d'ouvrir le Store ou d'installer le Worker : la réservation est alors marquée pour arrêt et le backend attend sa résolution. Lors de la fermeture de la fenêtre principale, le Desk ferme d'abord l'admission Start, demande l'arrêt de toutes les routes actives ou encore en démarrage et attend un cleanup borné avant de quitter. Il n'est donc pas nécessaire d'arrêter manuellement chaque route avant de fermer l'application.
|
||||||
|
|
||||||
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
|
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
|
||||||
|
|
||||||
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
|
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
|
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts";
|
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts";
|
||||||
@@ -9,6 +9,8 @@ export type FrontendLogTargetId = "frontend" | "main" | "splash";
|
|||||||
|
|
||||||
type ConsoleMethod = (...items: unknown[]) => void;
|
type ConsoleMethod = (...items: unknown[]) => void;
|
||||||
|
|
||||||
|
const FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024;
|
||||||
|
|
||||||
const originalConsole = {
|
const originalConsole = {
|
||||||
trace: console.trace.bind(console),
|
trace: console.trace.bind(console),
|
||||||
debug: console.debug.bind(console),
|
debug: console.debug.bind(console),
|
||||||
@@ -37,11 +39,15 @@ function formatMessage(items: unknown[]): string {
|
|||||||
return items.map(item => stringifyItem(item)).join(" ");
|
return items.map(item => stringifyItem(item)).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function boundMessage(message: string): string {
|
||||||
|
return message.length <= FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS ? message : `${message.slice(0, FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS)}…`;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
|
||||||
const payload: FrontendLogPayloadDto = {
|
const payload: FrontendLogPayloadDto = {
|
||||||
level,
|
level,
|
||||||
targetId,
|
targetId,
|
||||||
message,
|
message: boundMessage(message),
|
||||||
};
|
};
|
||||||
await invoke("emit_frontend_log", { payload });
|
await invoke("emit_frontend_log", { payload });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
|
//! Shared backend state owned by the Raw Transaction Ingest Desk Tauri application.
|
||||||
|
|
||||||
@@ -117,23 +117,26 @@ impl crate::AppState {
|
|||||||
|
|
||||||
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
|
/// Starts one independent route Worker after repeating complete Start-time revalidation, sharing Store only on an exact network match.
|
||||||
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
|
pub(crate) async fn start_route(&self, request: &crate::RawIngestRouteStartRequestDto) -> ksp_core_lib::Result<crate::RouteRuntimeLaunch> {
|
||||||
let generation = {
|
let admission = self.route_runtime.ensure_start_admission_open();
|
||||||
let generation = self.inventory_generation.lock();
|
if let std::result::Result::Err(error) = admission {
|
||||||
match generation {
|
return std::result::Result::Err(error);
|
||||||
std::result::Result::Ok(value) => *value,
|
}
|
||||||
std::result::Result::Err(_) => {
|
let generation = self.inventory_generation.lock();
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
let generation = match generation {
|
||||||
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
std::result::Result::Ok(value) => value,
|
||||||
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
std::result::Result::Err(_) => {
|
||||||
));
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
},
|
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
|
||||||
}
|
"Raw Transaction Ingest Desk route inventory generation lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let prepared = crate::prepare_route_start(&self.config_management, generation, request);
|
let prepared = crate::prepare_route_start(&self.config_management, *generation, request);
|
||||||
let prepared = match prepared {
|
let prepared = match prepared {
|
||||||
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),
|
||||||
};
|
};
|
||||||
|
std::mem::drop(generation);
|
||||||
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
|
return crate::start_route_runtime(std::sync::Arc::clone(&self.route_runtime), prepared).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +150,16 @@ impl crate::AppState {
|
|||||||
return self.route_runtime.stop_and_wait(request).await;
|
return self.route_runtime.stop_and_wait(request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Closes route Start admission once and requests cooperative Stop for all routes during application shutdown.
|
||||||
|
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||||
|
return self.route_runtime.begin_shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for bounded cleanup of all route Workers and the shared Store during application shutdown.
|
||||||
|
pub(crate) async fn shutdown_routes(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return self.route_runtime.shutdown_and_wait().await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds the safe scaffold status exposed by the shell.
|
/// Builds the safe scaffold status exposed by the shell.
|
||||||
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
|
||||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
|
//! Safe route-identity, Config-composability and lifecycle DTOs for Raw Transaction Ingest Desk.
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ impl crate::RawIngestCommitment {
|
|||||||
|
|
||||||
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
|
/// Safe request used to revalidate and reconstruct one future route Start without accepting physical resources from the frontend.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStartRequestDto.ts")]
|
||||||
pub(crate) struct RawIngestRouteStartRequestDto {
|
pub(crate) struct RawIngestRouteStartRequestDto {
|
||||||
/// Confirmed/finalized commitment selected for the future Worker.
|
/// Confirmed/finalized commitment selected for the future Worker.
|
||||||
@@ -114,7 +114,7 @@ pub(crate) struct RawIngestRouteStartRequestDto {
|
|||||||
|
|
||||||
/// Safe targeted Stop request containing only the logical Worker route identity.
|
/// Safe targeted Stop request containing only the logical Worker route identity.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/dto_route/RawIngestRouteStopRequestDto.ts")]
|
||||||
pub(crate) struct RawIngestRouteStopRequestDto {
|
pub(crate) struct RawIngestRouteStopRequestDto {
|
||||||
/// Safe logical network-profile identifier that owns the Worker route.
|
/// Safe logical network-profile identifier that owns the Worker route.
|
||||||
@@ -123,6 +123,36 @@ pub(crate) struct RawIngestRouteStopRequestDto {
|
|||||||
pub(crate) route_id: crate::RawIngestRouteId,
|
pub(crate) route_id: crate::RawIngestRouteId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256;
|
||||||
|
|
||||||
|
impl crate::RawIngestRouteStartRequestDto {
|
||||||
|
/// Validates the only caller-controlled free-form logical identifier before Config or runtime reconstruction.
|
||||||
|
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return validate_profile_id(self.profile_id.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawIngestRouteStopRequestDto {
|
||||||
|
/// Validates the only caller-controlled free-form logical identifier before runtime lookup.
|
||||||
|
pub(crate) fn validate_logical_identity(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return validate_profile_id(self.profile_id.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_profile_id(profile_id: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if profile_id.is_empty()
|
||||||
|
|| profile_id.len() > RAW_INGEST_PROFILE_ID_MAX_BYTES
|
||||||
|
|| profile_id.trim() != profile_id
|
||||||
|
|| profile_id.chars().any(char::is_control)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_ROUTE_REQUEST_INVALID,
|
||||||
|
"Raw Transaction Ingest Desk route request contains an invalid logical profile identifier",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
|
/// Safe acknowledgement that exact Start-time Transport resources were reconstructed and validated without Worker launch.
|
||||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Stable Raw Transaction Ingest Desk application error codes.
|
//! Stable Raw Transaction Ingest Desk application error codes.
|
||||||
|
|
||||||
@@ -17,6 +17,9 @@ pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode
|
|||||||
/// Frontend logging requested a target outside the application whitelist.
|
/// Frontend logging requested a target outside the application whitelist.
|
||||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode =
|
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode =
|
||||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_target_invalid");
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_target_invalid");
|
||||||
|
/// Frontend logging payload exceeds the bounded technical IPC contract.
|
||||||
|
pub(crate) const ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "frontend_log_payload_invalid");
|
||||||
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
|
/// Raw Transaction Ingest Desk could not install managed Logging or its safe fallback.
|
||||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
||||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "logging_bootstrap_failed");
|
||||||
@@ -46,6 +49,15 @@ pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STORE_SHUTDOWN_FAILED: ksp_core_lib::E
|
|||||||
/// The multi-route runtime reservation/handle state changed unexpectedly.
|
/// The multi-route runtime reservation/handle state changed unexpectedly.
|
||||||
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
|
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_STATE_INVALID: ksp_core_lib::ErrorCode =
|
||||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_state_invalid");
|
||||||
|
/// Application shutdown has closed admission for new route Worker starts.
|
||||||
|
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutting_down");
|
||||||
|
/// Application shutdown could not join all route Workers and shared Store cleanup within the bounded deadline.
|
||||||
|
pub(crate) const ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_runtime_shutdown_failed");
|
||||||
|
/// A Start/Stop IPC request contains an invalid logical identifier or unsupported shape.
|
||||||
|
pub(crate) const ERROR_CODE_ROUTE_REQUEST_INVALID: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "route_request_invalid");
|
||||||
/// Start request references an inventory generation that is no longer current.
|
/// Start request references an inventory generation that is no longer current.
|
||||||
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
|
pub(crate) const ERROR_CODE_ROUTE_START_STALE_INVENTORY: ksp_core_lib::ErrorCode =
|
||||||
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");
|
ksp_core_lib::ErrorCode::new("raw_transaction_ingest_desk", "stale_inventory");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
|
//! KSP-owned bridge for technical log events emitted by Raw Transaction Ingest Desk frontend scripts.
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ use ts_rs::TS; // rust-rules: trait-import
|
|||||||
|
|
||||||
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
|
/// Log payload sent by Raw Transaction Ingest Desk frontend scripts.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts")]
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_raw_transaction_ingest_desk/frontend_logging/FrontendLogPayloadDto.ts")]
|
||||||
pub(crate) struct FrontendLogPayloadDto {
|
pub(crate) struct FrontendLogPayloadDto {
|
||||||
/// Lowercase KSP log level.
|
/// Lowercase KSP log level.
|
||||||
@@ -34,8 +34,15 @@ enum FrontendLogTarget {
|
|||||||
Splash,
|
Splash,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192;
|
||||||
|
const FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16;
|
||||||
|
|
||||||
/// Emits one validated frontend event through the KSP Logging facade.
|
/// Emits one validated frontend event through the KSP Logging facade.
|
||||||
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||||
|
let shape = validate_payload_shape(&payload);
|
||||||
|
if let std::result::Result::Err(error) = shape {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
let level = parse_level(payload.level.as_str());
|
let level = parse_level(payload.level.as_str());
|
||||||
let level = match level {
|
let level = match level {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -50,26 +57,55 @@ pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_cor
|
|||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_payload_shape(payload: &FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
|
||||||
|
if payload.level.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|
||||||
|
|| payload.target_id.len() > FRONTEND_LOG_SELECTOR_MAX_BYTES
|
||||||
|
|| payload.message.len() > FRONTEND_LOG_MESSAGE_MAX_BYTES
|
||||||
|
|| payload.message.chars().any(char::is_control)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID,
|
||||||
|
"Frontend log payload exceeds the bounded technical IPC contract",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
|
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
|
||||||
return match level.trim().to_ascii_lowercase().as_str() {
|
let level = level.trim();
|
||||||
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
if level.eq_ignore_ascii_case("trace") {
|
||||||
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
return std::result::Result::Ok(FrontendLogLevel::Trace);
|
||||||
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
}
|
||||||
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
if level.eq_ignore_ascii_case("debug") {
|
||||||
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
return std::result::Result::Ok(FrontendLogLevel::Debug);
|
||||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
|
}
|
||||||
};
|
if level.eq_ignore_ascii_case("info") {
|
||||||
|
return std::result::Result::Ok(FrontendLogLevel::Info);
|
||||||
|
}
|
||||||
|
if level.eq_ignore_ascii_case("warn") {
|
||||||
|
return std::result::Result::Ok(FrontendLogLevel::Warn);
|
||||||
|
}
|
||||||
|
if level.eq_ignore_ascii_case("error") {
|
||||||
|
return std::result::Result::Ok(FrontendLogLevel::Error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
|
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
|
||||||
return match target_id.trim().to_ascii_lowercase().as_str() {
|
let target_id = target_id.trim();
|
||||||
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
|
if target_id.eq_ignore_ascii_case("frontend") {
|
||||||
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
|
return std::result::Result::Ok(FrontendLogTarget::Frontend);
|
||||||
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
|
}
|
||||||
_ => {
|
if target_id.eq_ignore_ascii_case("main") {
|
||||||
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
|
return std::result::Result::Ok(FrontendLogTarget::Main);
|
||||||
},
|
}
|
||||||
};
|
if target_id.eq_ignore_ascii_case("splash") {
|
||||||
|
return std::result::Result::Ok(FrontendLogTarget::Splash);
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID,
|
||||||
|
"Frontend log target identifier is not supported",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
|
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
|
//! Tauri desktop application scaffold for composing and supervising KSP live RAW transaction ingest routes.
|
||||||
|
|
||||||
@@ -113,18 +113,26 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
|||||||
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
|
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
|
||||||
/// Frontend logging requested an unsupported level.
|
/// Frontend logging requested an unsupported level.
|
||||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||||
|
/// Frontend logging payload exceeds the bounded technical IPC contract.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID;
|
||||||
/// Frontend logging requested an unsupported target.
|
/// Frontend logging requested an unsupported target.
|
||||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||||
/// Managed and fallback Logging initialization failed.
|
/// Managed and fallback Logging initialization failed.
|
||||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||||
/// Config-only route inventory cannot be projected safely.
|
/// Config-only route inventory cannot be projected safely.
|
||||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_INVENTORY_INVALID;
|
||||||
|
/// Start/Stop IPC request contains an invalid logical shape.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_REQUEST_INVALID;
|
||||||
/// A Worker runtime already owns the same logical route in the shared application runtime.
|
/// A Worker runtime already owns the same logical route in the shared application runtime.
|
||||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_ACTIVE;
|
||||||
/// Selected route cannot share the currently open Store because its network differs.
|
/// Selected route cannot share the currently open Store because its network differs.
|
||||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NETWORK_MISMATCH;
|
||||||
/// No selected route Worker runtime is currently active.
|
/// No selected route Worker runtime is currently active.
|
||||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE;
|
||||||
|
/// Application shutdown could not complete all bounded route/Store cleanup.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED;
|
||||||
|
/// Application shutdown has closed admission for new route Worker starts.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN;
|
||||||
/// Store or Worker startup failed after Start-time route revalidation.
|
/// Store or Worker startup failed after Start-time route revalidation.
|
||||||
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
|
pub(crate) use self::errors::ERROR_CODE_ROUTE_RUNTIME_START_FAILED;
|
||||||
/// The multi-route runtime reservation or handle state changed unexpectedly.
|
/// The multi-route runtime reservation or handle state changed unexpectedly.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
|
//! Multi-route Store and independent Worker lifecycle owned by Raw Transaction Ingest Desk.
|
||||||
|
|
||||||
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
|
const ROUTE_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||||
|
const ROUTE_STOP_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||||
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
|
const STORE_RECLAIM_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
|
||||||
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
|
const STORE_RECLAIM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ struct RouteRuntimeInner {
|
|||||||
network: std::option::Option<String>,
|
network: std::option::Option<String>,
|
||||||
next_sequence: u64,
|
next_sequence: u64,
|
||||||
routes: std::vec::Vec<RouteRuntimeSlot>,
|
routes: std::vec::Vec<RouteRuntimeSlot>,
|
||||||
|
shutting_down: bool,
|
||||||
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
|
store: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
|
||||||
store_closing_token: std::option::Option<u64>,
|
store_closing_token: std::option::Option<u64>,
|
||||||
store_opening_token: std::option::Option<u64>,
|
store_opening_token: std::option::Option<u64>,
|
||||||
@@ -34,6 +36,7 @@ impl crate::RouteRuntimeState {
|
|||||||
network: std::option::Option::None,
|
network: std::option::Option::None,
|
||||||
next_sequence: 0,
|
next_sequence: 0,
|
||||||
routes: std::vec::Vec::new(),
|
routes: std::vec::Vec::new(),
|
||||||
|
shutting_down: false,
|
||||||
store: std::option::Option::None,
|
store: std::option::Option::None,
|
||||||
store_closing_token: std::option::Option::None,
|
store_closing_token: std::option::Option::None,
|
||||||
store_opening_token: std::option::Option::None,
|
store_opening_token: std::option::Option::None,
|
||||||
@@ -41,12 +44,34 @@ impl crate::RouteRuntimeState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rejects new route Start work after application shutdown has closed admission.
|
||||||
|
pub(crate) fn ensure_start_admission_open(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let inner = self.inner.lock();
|
||||||
|
let inner = match inner {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
|
};
|
||||||
|
if inner.shutting_down {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
|
||||||
|
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
|
fn reserve(&self, prepared: &crate::PreparedRouteStart) -> ksp_core_lib::Result<RouteRuntimeReservation> {
|
||||||
let inner = self.inner.lock();
|
let inner = self.inner.lock();
|
||||||
let mut inner = match inner {
|
let mut inner = match inner {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
};
|
};
|
||||||
|
if inner.shutting_down {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN,
|
||||||
|
"Raw Transaction Ingest Desk cannot start a route while application shutdown is in progress",
|
||||||
|
));
|
||||||
|
}
|
||||||
if inner.store_closing_token.is_some() {
|
if inner.store_closing_token.is_some() {
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
|
crate::ERROR_CODE_ROUTE_RUNTIME_ACTIVE,
|
||||||
@@ -111,7 +136,7 @@ impl crate::RouteRuntimeState {
|
|||||||
if needs_store_open {
|
if needs_store_open {
|
||||||
inner.store_opening_token = std::option::Option::Some(sequence);
|
inner.store_opening_token = std::option::Option::Some(sequence);
|
||||||
}
|
}
|
||||||
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), token: sequence });
|
inner.routes.push(RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: sequence });
|
||||||
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
|
return std::result::Result::Ok(RouteRuntimeReservation { identity, needs_store_open, token: sequence, worker_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +198,19 @@ impl crate::RouteRuntimeState {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let state = project_worker_state(handle.snapshot_source().current().worker_snapshot().state());
|
let stop_requested = match &inner.routes[position] {
|
||||||
|
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested,
|
||||||
|
RouteRuntimeSlot::Active { .. } => false,
|
||||||
|
};
|
||||||
|
let should_stop = stop_requested || inner.shutting_down;
|
||||||
|
if should_stop {
|
||||||
|
let _accepted = handle.request_stop();
|
||||||
|
}
|
||||||
|
let state = if should_stop {
|
||||||
|
crate::RawIngestRouteState::Stopping
|
||||||
|
} else {
|
||||||
|
project_worker_state(handle.snapshot_source().current().worker_snapshot().state())
|
||||||
|
};
|
||||||
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
|
inner.routes[position] = RouteRuntimeSlot::Active { handle, identity: reservation.identity.clone(), token: reservation.token };
|
||||||
return std::result::Result::Ok(reservation.identity.dto(state));
|
return std::result::Result::Ok(reservation.identity.dto(state));
|
||||||
}
|
}
|
||||||
@@ -184,7 +221,12 @@ impl crate::RouteRuntimeState {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
};
|
};
|
||||||
|
let cancelled_terminal = inner.routes.iter().find(|slot| return slot.token() == token).and_then(RouteRuntimeSlot::cancelled_start_terminal);
|
||||||
inner.routes.retain(|slot| return slot.token() != token);
|
inner.routes.retain(|slot| return slot.token() != token);
|
||||||
|
if let std::option::Option::Some(terminal) = cancelled_terminal {
|
||||||
|
inner.last_terminals.retain(|previous| return previous.profile_id != terminal.profile_id || previous.route_id != terminal.route_id);
|
||||||
|
inner.last_terminals.push(terminal);
|
||||||
|
}
|
||||||
if inner.store_opening_token == std::option::Option::Some(token) {
|
if inner.store_opening_token == std::option::Option::Some(token) {
|
||||||
inner.store_opening_token = std::option::Option::None;
|
inner.store_opening_token = std::option::Option::None;
|
||||||
}
|
}
|
||||||
@@ -272,8 +314,71 @@ impl crate::RouteRuntimeState {
|
|||||||
return std::result::Result::Ok(values);
|
return std::result::Result::Ok(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Closes route Start admission exactly once and requests cooperative Stop for all active or still-starting routes.
|
||||||
|
pub(crate) fn begin_shutdown(&self) -> ksp_core_lib::Result<bool> {
|
||||||
|
let inner = self.inner.lock();
|
||||||
|
let mut inner = match inner {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
|
};
|
||||||
|
if inner.shutting_down {
|
||||||
|
return std::result::Result::Ok(false);
|
||||||
|
}
|
||||||
|
inner.shutting_down = true;
|
||||||
|
for slot in &mut inner.routes {
|
||||||
|
match slot {
|
||||||
|
RouteRuntimeSlot::Starting { stop_requested, .. } => *stop_requested = true,
|
||||||
|
RouteRuntimeSlot::Active { handle, .. } => {
|
||||||
|
let _accepted = handle.request_stop();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until all owned route Workers and the shared Store have completed bounded application-shutdown cleanup.
|
||||||
|
pub(crate) async fn shutdown_and_wait(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let begin = self.begin_shutdown();
|
||||||
|
if let std::result::Result::Err(error) = begin {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
loop {
|
||||||
|
let complete = self.shutdown_complete();
|
||||||
|
let complete = match complete {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if complete {
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
if started.elapsed() >= ROUTE_SHUTDOWN_TIMEOUT {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED,
|
||||||
|
"Raw Transaction Ingest Desk application shutdown did not release all route Workers and shared Store state before the bounded deadline",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tokio::time::sleep(STORE_RECLAIM_POLL_INTERVAL).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_complete(&self) -> ksp_core_lib::Result<bool> {
|
||||||
|
let inner = self.inner.lock();
|
||||||
|
let inner = match inner {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(
|
||||||
|
inner.routes.is_empty() && inner.store.is_none() && inner.store_opening_token.is_none() && inner.store_closing_token.is_none(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
|
/// Requests cooperative Stop for one exact logical route and waits for its Worker cleanup.
|
||||||
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
pub(crate) async fn stop_and_wait(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<crate::RawIngestRouteRuntimeDto> {
|
||||||
|
let identity = request.validate_logical_identity();
|
||||||
|
if let std::result::Result::Err(error) = identity {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
let target = self.stop_target(request);
|
let target = self.stop_target(request);
|
||||||
let target = match target {
|
let target = match target {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -281,6 +386,7 @@ impl crate::RouteRuntimeState {
|
|||||||
};
|
};
|
||||||
let (handle, identity, token) = match target {
|
let (handle, identity, token) = match target {
|
||||||
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
|
RouteStopTarget::Active { handle, identity, token } => (handle, identity, token),
|
||||||
|
RouteStopTarget::Starting { identity, token } => return self.wait_route_cleanup(token, &identity).await,
|
||||||
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
|
RouteStopTarget::Terminal(value) => return std::result::Result::Ok(value),
|
||||||
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
|
RouteStopTarget::TerminalClosing => return self.wait_terminal_store_cleanup(request).await,
|
||||||
};
|
};
|
||||||
@@ -296,33 +402,30 @@ impl crate::RouteRuntimeState {
|
|||||||
|
|
||||||
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
|
fn stop_target(&self, request: &crate::RawIngestRouteStopRequestDto) -> ksp_core_lib::Result<RouteStopTarget> {
|
||||||
let inner = self.inner.lock();
|
let inner = self.inner.lock();
|
||||||
let inner = match inner {
|
let mut inner = match inner {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
std::result::Result::Err(_) => return std::result::Result::Err(runtime_lock_error()),
|
||||||
};
|
};
|
||||||
let slot = inner.routes.iter().find(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
|
let position = inner.routes.iter().position(|slot| return slot.matches_identity(request.profile_id.as_str(), request.route_id));
|
||||||
return match slot {
|
if let std::option::Option::Some(position) = position {
|
||||||
std::option::Option::Some(RouteRuntimeSlot::Active { handle, identity, token }) => {
|
return match &mut inner.routes[position] {
|
||||||
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
|
RouteRuntimeSlot::Active { handle, identity, token } => {
|
||||||
},
|
std::result::Result::Ok(RouteStopTarget::Active { handle: handle.clone(), identity: identity.clone(), token: *token })
|
||||||
std::option::Option::Some(RouteRuntimeSlot::Starting { .. }) => std::result::Result::Err(ksp_core_lib::Error::new(
|
},
|
||||||
|
RouteRuntimeSlot::Starting { identity, stop_requested, token } => {
|
||||||
|
*stop_requested = true;
|
||||||
|
std::result::Result::Ok(RouteStopTarget::Starting { identity: identity.clone(), token: *token })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let terminal = inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
|
||||||
|
return match terminal {
|
||||||
|
std::option::Option::Some(value) if inner.store_closing_token.is_none() => std::result::Result::Ok(RouteStopTarget::Terminal(value.clone())),
|
||||||
|
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
|
||||||
|
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
|
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
|
||||||
"Raw Transaction Ingest Desk route Worker is still starting and cannot be stopped yet",
|
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
|
||||||
)),
|
)),
|
||||||
std::option::Option::None => {
|
|
||||||
let terminal =
|
|
||||||
inner.last_terminals.iter().find(|terminal| return terminal.profile_id == request.profile_id && terminal.route_id == request.route_id);
|
|
||||||
match terminal {
|
|
||||||
std::option::Option::Some(value) if inner.store_closing_token.is_none() => {
|
|
||||||
std::result::Result::Ok(RouteStopTarget::Terminal(value.clone()))
|
|
||||||
},
|
|
||||||
std::option::Option::Some(_) => std::result::Result::Ok(RouteStopTarget::TerminalClosing),
|
|
||||||
std::option::Option::None => std::result::Result::Err(ksp_core_lib::Error::new(
|
|
||||||
crate::ERROR_CODE_ROUTE_RUNTIME_NOT_ACTIVE,
|
|
||||||
"Raw Transaction Ingest Desk has no active Worker for the selected logical route",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,6 +771,10 @@ enum RouteStopTarget {
|
|||||||
identity: RouteRuntimeIdentity,
|
identity: RouteRuntimeIdentity,
|
||||||
token: u64,
|
token: u64,
|
||||||
},
|
},
|
||||||
|
Starting {
|
||||||
|
identity: RouteRuntimeIdentity,
|
||||||
|
token: u64,
|
||||||
|
},
|
||||||
Terminal(crate::RawIngestRouteRuntimeDto),
|
Terminal(crate::RawIngestRouteRuntimeDto),
|
||||||
TerminalClosing,
|
TerminalClosing,
|
||||||
}
|
}
|
||||||
@@ -682,6 +789,7 @@ struct RouteRuntimeReservation {
|
|||||||
enum RouteRuntimeSlot {
|
enum RouteRuntimeSlot {
|
||||||
Starting {
|
Starting {
|
||||||
identity: RouteRuntimeIdentity,
|
identity: RouteRuntimeIdentity,
|
||||||
|
stop_requested: bool,
|
||||||
token: u64,
|
token: u64,
|
||||||
},
|
},
|
||||||
Active {
|
Active {
|
||||||
@@ -706,6 +814,13 @@ impl RouteRuntimeSlot {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cancelled_start_terminal(&self) -> std::option::Option<crate::RawIngestRouteRuntimeDto> {
|
||||||
|
return match self {
|
||||||
|
Self::Starting { identity, stop_requested: true, .. } => std::option::Option::Some(identity.dto(crate::RawIngestRouteState::Stopped)),
|
||||||
|
Self::Starting { stop_requested: false, .. } | Self::Active { .. } => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn token(&self) -> u64 {
|
fn token(&self) -> u64 {
|
||||||
return match self {
|
return match self {
|
||||||
Self::Starting { token, .. } | Self::Active { token, .. } => *token,
|
Self::Starting { token, .. } | Self::Active { token, .. } => *token,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
|
//! Start-time Config revalidation and exact Transport/Store resource reconstruction for route validation and runtime launch.
|
||||||
|
|
||||||
@@ -27,6 +27,10 @@ pub(crate) fn prepare_route_start(
|
|||||||
current_generation: u32,
|
current_generation: u32,
|
||||||
request: &crate::RawIngestRouteStartRequestDto,
|
request: &crate::RawIngestRouteStartRequestDto,
|
||||||
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
|
) -> ksp_core_lib::Result<crate::PreparedRouteStart> {
|
||||||
|
let identity = request.validate_logical_identity();
|
||||||
|
if let std::result::Result::Err(error) = identity {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
if current_generation == 0 || request.inventory_generation != current_generation {
|
if current_generation == 0 || request.inventory_generation != current_generation {
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||||
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,
|
crate::ERROR_CODE_ROUTE_START_STALE_INVENTORY,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
|
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
|||||||
builder = builder.manage(app_state);
|
builder = builder.manage(app_state);
|
||||||
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
|
builder = builder.plugin(tauri_plugin_tracing::Builder::new().build::<tauri::Wry>());
|
||||||
builder = configure_commands(builder);
|
builder = configure_commands(builder);
|
||||||
|
builder = configure_window_events(builder);
|
||||||
builder = builder.setup(|app| {
|
builder = builder.setup(|app| {
|
||||||
let splash = crate::require_splash_window(app);
|
let splash = crate::require_splash_window(app);
|
||||||
if let std::result::Result::Err(error) = splash {
|
if let std::result::Result::Err(error) = splash {
|
||||||
@@ -90,6 +91,66 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn configure_window_events(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
|
return builder.on_window_event(|window, event| {
|
||||||
|
if window.label() != "main" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
api.prevent_close();
|
||||||
|
let app_handle = window.app_handle().clone();
|
||||||
|
let state = app_handle.state::<crate::AppState>();
|
||||||
|
let begin = state.begin_shutdown();
|
||||||
|
let begin = match begin {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
error_domain = error.code().domain(),
|
||||||
|
error_code = error.code().code(),
|
||||||
|
"Raw Transaction Ingest Desk could not enter graceful shutdown; exiting after safe error projection"
|
||||||
|
);
|
||||||
|
app_handle.exit(1);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if !begin {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
"Raw Transaction Ingest Desk main-window close requested; route Start admission closed and cooperative shutdown requested"
|
||||||
|
);
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let state = app_handle.state::<crate::AppState>();
|
||||||
|
let shutdown = state.shutdown_routes().await;
|
||||||
|
match shutdown {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
"Raw Transaction Ingest Desk route and shared Store shutdown completed; exiting application"
|
||||||
|
);
|
||||||
|
app_handle.exit(0);
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
domain = crate::TRACING_DOMAIN_WINDOWS,
|
||||||
|
error_domain = error.code().domain(),
|
||||||
|
error_code = error.code().code(),
|
||||||
|
"Raw Transaction Ingest Desk bounded route shutdown failed; exiting application after bounded cleanup attempt"
|
||||||
|
);
|
||||||
|
app_handle.exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
|
||||||
ksp_logging_lib::warn!(
|
ksp_logging_lib::warn!(
|
||||||
target: crate::TRACING_TARGET,
|
target: crate::TRACING_TARGET,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
|
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
|
||||||
|
|
||||||
@@ -274,3 +274,36 @@ fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_rout
|
|||||||
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
|
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned() {
|
||||||
|
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||||
|
for required in [
|
||||||
|
"configure_window_events(builder)",
|
||||||
|
"tauri::WindowEvent::CloseRequested",
|
||||||
|
"api.prevent_close()",
|
||||||
|
"state.begin_shutdown()",
|
||||||
|
"state.shutdown_routes().await",
|
||||||
|
"app_handle.exit(0)",
|
||||||
|
"app_handle.exit(1)",
|
||||||
|
] {
|
||||||
|
assert!(tauri.contains(required), "missing pre.012 bounded shutdown marker {required}");
|
||||||
|
}
|
||||||
|
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||||
|
for required in [
|
||||||
|
"shutting_down: bool",
|
||||||
|
"stop_requested: bool",
|
||||||
|
"ROUTE_SHUTDOWN_TIMEOUT",
|
||||||
|
"ensure_start_admission_open",
|
||||||
|
"shutdown_and_wait",
|
||||||
|
"RouteStopTarget::Starting",
|
||||||
|
"*stop_requested = true",
|
||||||
|
"handle.request_stop()",
|
||||||
|
] {
|
||||||
|
assert!(runtime.contains(required), "missing pre.012 route-race marker {required}");
|
||||||
|
}
|
||||||
|
let app_state = read_text(app_root().join("src/app_state.rs").as_path());
|
||||||
|
assert!(app_state.contains("let generation = self.inventory_generation.lock()"));
|
||||||
|
assert!(app_state.contains("std::mem::drop(generation)"));
|
||||||
|
assert!(app_state.contains("self.route_runtime.ensure_start_admission_open()"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
|
//! Desktop security canaries for the Raw Transaction Ingest Desk scaffold.
|
||||||
|
|
||||||
@@ -195,3 +195,31 @@ fn pre_011_frontend_monitoring_tracing_is_source_neutral_and_does_not_log_raw_ma
|
|||||||
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
|
assert!(!main.contains("frontendTrace(\"main\", \"Raw Transaction Ingest Desk route monitoring latest value applied\", status"));
|
||||||
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
|
assert!(!main.contains("frontendDebug(\"main\", \"Raw Transaction Ingest Desk route monitoring resynchronization completed\", statuses"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected() {
|
||||||
|
let dto = read_text(app_root().join("src/dto_route.rs").as_path());
|
||||||
|
assert!(dto.matches("deny_unknown_fields").count() >= 2);
|
||||||
|
assert!(dto.contains("RAW_INGEST_PROFILE_ID_MAX_BYTES: usize = 256"));
|
||||||
|
assert!(dto.contains("validate_logical_identity"));
|
||||||
|
assert!(dto.contains("ERROR_CODE_ROUTE_REQUEST_INVALID"));
|
||||||
|
let frontend_logging = read_text(app_root().join("src/frontend_logging.rs").as_path());
|
||||||
|
for required in [
|
||||||
|
"deny_unknown_fields",
|
||||||
|
"FRONTEND_LOG_MESSAGE_MAX_BYTES: usize = 8192",
|
||||||
|
"FRONTEND_LOG_SELECTOR_MAX_BYTES: usize = 16",
|
||||||
|
"payload.message.chars().any(char::is_control)",
|
||||||
|
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
|
||||||
|
"eq_ignore_ascii_case",
|
||||||
|
] {
|
||||||
|
assert!(frontend_logging.contains(required), "missing pre.012 frontend IPC guard {required}");
|
||||||
|
}
|
||||||
|
assert!(!frontend_logging.contains("to_ascii_lowercase()"));
|
||||||
|
let frontend = read_text(app_root().join("frontend/ts/frontend_log.ts").as_path());
|
||||||
|
assert!(frontend.contains("FRONTEND_LOG_MESSAGE_MAX_CODE_UNITS = 1024"));
|
||||||
|
assert!(frontend.contains("boundMessage(message)"));
|
||||||
|
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||||
|
assert!(tauri.contains("error_domain = error.code().domain()"));
|
||||||
|
assert!(tauri.contains("error_code = error.code().code()"));
|
||||||
|
assert!(!tauri.contains("error = ?error"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
||||||
// version: 11
|
// version: 12
|
||||||
|
|
||||||
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
|
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
|
||||||
|
|
||||||
@@ -200,3 +200,27 @@ fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth
|
|||||||
assert!(main.contains("renderRouteMonitoringCard"));
|
assert!(main.contains("renderRouteMonitoringCard"));
|
||||||
assert!(main.contains("renderSelectedProfile"));
|
assert!(main.contains("renderSelectedProfile"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth() {
|
||||||
|
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||||
|
assert_eq!(lib.matches("mod ").count(), 16);
|
||||||
|
for required in [
|
||||||
|
"ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID",
|
||||||
|
"ERROR_CODE_ROUTE_REQUEST_INVALID",
|
||||||
|
"ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN",
|
||||||
|
"ERROR_CODE_ROUTE_RUNTIME_SHUTDOWN_FAILED",
|
||||||
|
] {
|
||||||
|
assert!(lib.contains(required), "missing pre.012 crate-root error export {required}");
|
||||||
|
}
|
||||||
|
let runtime = read_text(app_root().join("src/route_runtime.rs").as_path());
|
||||||
|
assert!(runtime.contains("stop_requested: bool"));
|
||||||
|
assert!(runtime.contains("RouteStopTarget::Starting"));
|
||||||
|
assert!(runtime.contains("cancelled_start_terminal"));
|
||||||
|
assert!(runtime.contains("inner.routes.is_empty()"));
|
||||||
|
let route_start = read_text(app_root().join("src/route_start.rs").as_path());
|
||||||
|
assert!(route_start.contains("build_route_inventory_with_environment"));
|
||||||
|
assert!(route_start.contains("request.validate_logical_identity()"));
|
||||||
|
let worker_manifest = read_text(app_root().join("../ksp-worker-raw-transaction-ingest-lib/Cargo.toml").as_path());
|
||||||
|
assert!(!worker_manifest.contains("ksp-app-raw-transaction-ingest-desk"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_005_route_ids_are_exact_and_stably_ordered() {
|
fn pre_005_route_ids_are_exact_and_stably_ordered() {
|
||||||
@@ -91,3 +91,34 @@ fn pre_008_runtime_acknowledgement_serializes_only_safe_logical_identity_and_sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity() {
|
||||||
|
let start = serde_json::from_value::<crate::RawIngestRouteStartRequestDto>(serde_json::json!({
|
||||||
|
"commitment": "confirmed",
|
||||||
|
"inventoryGeneration": 1,
|
||||||
|
"profileId": "mainnet",
|
||||||
|
"routeId": "http-block-polling",
|
||||||
|
"endpointUrl": "https://forbidden.invalid"
|
||||||
|
}));
|
||||||
|
assert!(start.is_err());
|
||||||
|
let stop = serde_json::from_value::<crate::RawIngestRouteStopRequestDto>(serde_json::json!({
|
||||||
|
"profileId": "mainnet",
|
||||||
|
"routeId": "http-block-polling",
|
||||||
|
"workerId": "forbidden"
|
||||||
|
}));
|
||||||
|
assert!(stop.is_err());
|
||||||
|
let oversized = crate::RawIngestRouteStartRequestDto {
|
||||||
|
commitment: crate::RawIngestCommitment::Confirmed,
|
||||||
|
inventory_generation: 1,
|
||||||
|
profile_id: "x".repeat(super::RAW_INGEST_PROFILE_ID_MAX_BYTES + 1),
|
||||||
|
route_id: crate::RawIngestRouteId::HttpBlockPolling,
|
||||||
|
};
|
||||||
|
let oversized = oversized.validate_logical_identity();
|
||||||
|
assert!(oversized.is_err());
|
||||||
|
if let std::result::Result::Err(error) = oversized {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_REQUEST_INVALID);
|
||||||
|
}
|
||||||
|
let control = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet\n".to_owned(), route_id: crate::RawIngestRouteId::HttpBlockPolling };
|
||||||
|
assert!(control.validate_logical_identity().is_err());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn frontend_log_levels_are_explicit_and_case_insensitive() {
|
fn frontend_log_levels_are_explicit_and_case_insensitive() {
|
||||||
@@ -38,3 +38,26 @@ fn arbitrary_frontend_log_target_is_rejected() {
|
|||||||
};
|
};
|
||||||
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
|
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages() {
|
||||||
|
let unknown = serde_json::from_value::<crate::FrontendLogPayloadDto>(serde_json::json!({
|
||||||
|
"level": "info",
|
||||||
|
"targetId": "main",
|
||||||
|
"message": "safe",
|
||||||
|
"endpointUrl": "https://forbidden.invalid"
|
||||||
|
}));
|
||||||
|
assert!(unknown.is_err());
|
||||||
|
let oversized = crate::FrontendLogPayloadDto {
|
||||||
|
level: "info".to_owned(),
|
||||||
|
target_id: "main".to_owned(),
|
||||||
|
message: "x".repeat(super::FRONTEND_LOG_MESSAGE_MAX_BYTES + 1),
|
||||||
|
};
|
||||||
|
let result = super::validate_payload_shape(&oversized);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_PAYLOAD_INVALID);
|
||||||
|
}
|
||||||
|
let nul = crate::FrontendLogPayloadDto { level: "info".to_owned(), target_id: "main".to_owned(), message: "safe\0hostile".to_owned() };
|
||||||
|
assert!(super::validate_payload_shape(&nul).is_err());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
|
// file: crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
|
fn pre_009_multi_route_runtime_starts_without_workers_or_shared_store_material() {
|
||||||
@@ -63,3 +63,70 @@ fn pre_009_late_targeted_stop_waits_while_final_shared_store_is_closing() {
|
|||||||
let target = state.stop_target(&request);
|
let target = state.stop_target(&request);
|
||||||
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
|
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::TerminalClosing)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop() {
|
||||||
|
let state = crate::RouteRuntimeState::new();
|
||||||
|
let identity = super::RouteRuntimeIdentity {
|
||||||
|
commitment: crate::RawIngestCommitment::Confirmed,
|
||||||
|
inventory_generation: 12,
|
||||||
|
network: "mainnet".to_owned(),
|
||||||
|
profile_id: "mainnet".to_owned(),
|
||||||
|
route_id: crate::RawIngestRouteId::YellowstoneHydrated,
|
||||||
|
};
|
||||||
|
let inner = state.inner.lock();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
if let std::result::Result::Ok(mut inner) = inner {
|
||||||
|
inner.network = std::option::Option::Some("mainnet".to_owned());
|
||||||
|
inner.routes.push(super::RouteRuntimeSlot::Starting { identity: identity.clone(), stop_requested: false, token: 12 });
|
||||||
|
}
|
||||||
|
let request = crate::RawIngestRouteStopRequestDto { profile_id: "mainnet".to_owned(), route_id: crate::RawIngestRouteId::YellowstoneHydrated };
|
||||||
|
let target = state.stop_target(&request);
|
||||||
|
assert!(matches!(target, std::result::Result::Ok(super::RouteStopTarget::Starting { token: 12, .. })));
|
||||||
|
let inner = state.inner.lock();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
if let std::result::Result::Ok(inner) = inner {
|
||||||
|
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
|
||||||
|
}
|
||||||
|
let rollback = state.rollback(12);
|
||||||
|
assert!(matches!(rollback, std::result::Result::Ok(std::option::Option::None)));
|
||||||
|
let inner = state.inner.lock();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
if let std::result::Result::Ok(inner) = inner {
|
||||||
|
assert!(inner.routes.is_empty());
|
||||||
|
assert!(matches!(inner.last_terminals.first(), std::option::Option::Some(value) if value.state == crate::RawIngestRouteState::Stopped));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation() {
|
||||||
|
let state = crate::RouteRuntimeState::new();
|
||||||
|
let identity = super::RouteRuntimeIdentity {
|
||||||
|
commitment: crate::RawIngestCommitment::Finalized,
|
||||||
|
inventory_generation: 13,
|
||||||
|
network: "mainnet".to_owned(),
|
||||||
|
profile_id: "mainnet".to_owned(),
|
||||||
|
route_id: crate::RawIngestRouteId::HttpBlockPolling,
|
||||||
|
};
|
||||||
|
let inner = state.inner.lock();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
if let std::result::Result::Ok(mut inner) = inner {
|
||||||
|
inner.network = std::option::Option::Some("mainnet".to_owned());
|
||||||
|
inner.routes.push(super::RouteRuntimeSlot::Starting { identity, stop_requested: false, token: 13 });
|
||||||
|
}
|
||||||
|
let first = state.begin_shutdown();
|
||||||
|
assert!(matches!(first, std::result::Result::Ok(true)));
|
||||||
|
let second = state.begin_shutdown();
|
||||||
|
assert!(matches!(second, std::result::Result::Ok(false)));
|
||||||
|
let admission = state.ensure_start_admission_open();
|
||||||
|
assert!(admission.is_err());
|
||||||
|
if let std::result::Result::Err(error) = admission {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_ROUTE_RUNTIME_SHUTTING_DOWN);
|
||||||
|
}
|
||||||
|
let inner = state.inner.lock();
|
||||||
|
assert!(inner.is_ok());
|
||||||
|
if let std::result::Result::Ok(inner) = inner {
|
||||||
|
assert!(inner.shutting_down);
|
||||||
|
assert!(matches!(inner.routes.first(), std::option::Option::Some(super::RouteRuntimeSlot::Starting { stop_requested: true, .. })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
177
deltas/0.3.15/pre.012.md
Normal file
177
deltas/0.3.15/pre.012.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<!-- file: deltas/0.3.15/pre.012.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.15-pre.012`
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
base archive : ksp-general-0.3.15-pre.011-fix.001.zip
|
||||||
|
base SHA-256 : 5da2d7bf343d20354b941ef9158004c3dabf521251bb346ef3aaf20586342a04
|
||||||
|
base version : 0.3.15-pre.11.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur du 16 septembre 2026 confirme la base : audits Rust/Markdown, `cargo check --workspace`, Clippy strict, tests Raw Transaction Ingest Desk/Worker et workspace all-targets/all-features passent. Le live Mainnet valide aussi plusieurs cycles Yellowstone + HTTP Block Polling simultanés avec Stop ciblé dans les deux ordres et fermeture du Store seulement après la dernière route.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Fermer les races Start/Stop, stale inventory, shutdown applicatif et les surfaces IPC hostiles sans modifier Config, Transport, Store, Worker, Common RAW ni les stratégies d'acquisition.
|
||||||
|
|
||||||
|
## Race Start / Stop
|
||||||
|
|
||||||
|
Une route réservée mais pas encore activée possède maintenant un marqueur `stop_requested`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Stop sur Active -> request_stop Worker puis attente terminal/cleanup
|
||||||
|
Stop sur Starting -> stop_requested=true puis attente cleanup
|
||||||
|
Start active après Stop -> request_stop avant acknowledgement
|
||||||
|
Start échoue avant activation -> rollback retient terminal Stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
La route n'est donc plus faussement déclarée `not_active` pendant sa fenêtre de démarrage. Le timeout de cleanup Desk est porté à `15 s`, au-dessus du drain Worker borné, afin de ne pas transformer un arrêt coopératif encore valide en erreur Desk prématurée.
|
||||||
|
|
||||||
|
## Shutdown applicatif
|
||||||
|
|
||||||
|
`RouteRuntimeState` possède désormais un état `shutting_down` one-shot. Le `CloseRequested` de la fenêtre principale est intercepté :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prevent_close
|
||||||
|
begin_shutdown
|
||||||
|
fermeture admission Start
|
||||||
|
marquage des Starting
|
||||||
|
request_stop de tous les Active
|
||||||
|
attente bornée routes + Store
|
||||||
|
exit(0) si cleanup complet
|
||||||
|
exit(1) si échec/timeout sûr
|
||||||
|
```
|
||||||
|
|
||||||
|
Un second événement de fermeture pendant ce cycle ne lance pas une seconde séquence concurrente. L'admission shutdown est vérifiée avant la préparation Start puis atomiquement lors de la réservation runtime, ce qui ferme la race entre un Start déjà en vol et la fermeture de l'application.
|
||||||
|
|
||||||
|
## Stale inventory
|
||||||
|
|
||||||
|
Le Start conserve le verrou de génération pendant toute la préparation synchrone qui revalide l'inventaire et reconstruit Config/Transport. Le verrou est explicitement libéré avant le premier `await` d'ouverture Store. Un refresh d'inventaire ne peut donc pas intercaler une nouvelle génération entre le contrôle de génération et cette reconstruction.
|
||||||
|
|
||||||
|
## Sécurité IPC
|
||||||
|
|
||||||
|
Les requêtes Start/Stop sont maintenant `deny_unknown_fields` et valident `profile_id` avant reconstruction :
|
||||||
|
|
||||||
|
```text
|
||||||
|
non vide
|
||||||
|
<= 256 octets
|
||||||
|
aucun trim implicite
|
||||||
|
aucun caractère de contrôle
|
||||||
|
```
|
||||||
|
|
||||||
|
Le bridge frontend Logging est également shape-strict et borné :
|
||||||
|
|
||||||
|
```text
|
||||||
|
level / target <= 16 octets
|
||||||
|
message <= 8192 octets
|
||||||
|
aucun caractère de contrôle
|
||||||
|
aucune allocation lowercase du selector hostile
|
||||||
|
```
|
||||||
|
|
||||||
|
Le frontend borne le message technique à `1024` code units avant IPC, ce qui laisse une marge sûre pour l'encodage UTF-8. Les erreurs Tauri restent projetées uniquement par `domain/code` ; les payloads hostiles ne sont jamais journalisés.
|
||||||
|
|
||||||
|
## Canaris
|
||||||
|
|
||||||
|
Ajouts/renforcements :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity
|
||||||
|
pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages
|
||||||
|
pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop
|
||||||
|
pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation
|
||||||
|
pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned
|
||||||
|
pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected
|
||||||
|
pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari de race Start/Stop vérifie aussi que le rollback d'une réservation déjà stoppée retient un terminal logique `Stopped`.
|
||||||
|
|
||||||
|
## Frontières inchangées
|
||||||
|
|
||||||
|
Aucune modification de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
ksp-onchain-transport-lib
|
||||||
|
ksp-store-lib / backend Store
|
||||||
|
ksp-worker-raw-transaction-ingest-lib
|
||||||
|
ksp-raw-transaction-lib
|
||||||
|
pipeline RAW / replay / repair
|
||||||
|
```
|
||||||
|
|
||||||
|
Le module Rust de production du Desk reste à `16` modules et aucun nouvel edge lower-layer n'est introduit.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version : 0.3.15-pre.12
|
||||||
|
root Cargo header counter : 622
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation d'assemblage
|
||||||
|
|
||||||
|
Exécuté 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.15
|
||||||
|
canaris statiques pre.012 de surface/race/IPC
|
||||||
|
TypeScript auxiliary no-emit du frontend_log avec bindings temporaires
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo/rustfmt ne sont pas disponibles dans cet environnement ; aucun gate Cargo de `pre.012` n'est déclaré PASS ici.
|
||||||
|
|
||||||
|
Le gate opérateur attendu reste :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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.15
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
|
||||||
|
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `npm run build` manuel n'est demandé.
|
||||||
|
|
||||||
|
## Inventaire exact du delta
|
||||||
|
|
||||||
|
Ajout :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.15/pre.012.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifications :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/README.md
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/USAGE.md
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/frontend_log.ts
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/app_state.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/dto_route.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/errors.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/frontend_logging.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/lib.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/route_runtime.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/route_start.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_security.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/dto_route.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/frontend_logging.rs
|
||||||
|
crates/ksp-app-raw-transaction-ingest-desk/unit_tests/route_runtime.rs
|
||||||
|
docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md
|
||||||
|
docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Suppressions : aucune.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
|
<!-- file: docs/plans/036-V0_3_15_RAW_TRANSACTION_INGEST_DESK_PLAN.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# Plan v0.3.15 — Raw Transaction Ingest Desk
|
# Plan v0.3.15 — Raw Transaction Ingest Desk
|
||||||
|
|
||||||
@@ -835,7 +835,15 @@ Budget cible : une tranche frontend.
|
|||||||
|
|
||||||
### `pre.012` — races / shutdown / sécurité IPC
|
### `pre.012` — races / shutdown / sécurité IPC
|
||||||
|
|
||||||
Durcir stale inventory, Start/Stop concurrents, fermeture application, joins bornés, erreurs sûres, surfaces IPC et scénarios hostiles.
|
**État : implémenté.** Le Desk ferme désormais explicitement l'admission de nouveaux Start dès qu'une fermeture de la fenêtre principale est engagée. La fermeture Tauri est interceptée une seule fois, demande un Stop coopératif à toutes les routes actives, marque aussi les réservations encore en `Starting`, puis attend de façon bornée la disparition de tous les Workers et du Store partagé avant de quitter le processus. Un second événement de fermeture ne lance pas un second shutdown concurrent.
|
||||||
|
|
||||||
|
La race Start/Stop est désormais définie : un Stop ciblé reçu pendant `Starting` marque la réservation `stop_requested` au lieu de répondre `not_active`. Si le Worker est ensuite activé, son Stop est demandé avant publication de l'acknowledgement ; si le Start échoue avant activation, le rollback conserve un terminal logique `Stopped`, ce qui permet au Stop concurrent de terminer sans inventer de Worker. Les délais de cleanup Desk sont alignés sur les drains Worker bornés afin de ne pas signaler prématurément un échec pendant un arrêt coopératif valide.
|
||||||
|
|
||||||
|
La revalidation Start conserve la génération d'inventaire sous verrou pendant toute la reconstruction synchrone Config/Transport, puis libère explicitement ce verrou avant le premier `await`. L'admission shutdown est vérifiée avant la préparation et de nouveau atomiquement lors de la réservation runtime ; un Start déjà en vol ne peut donc pas franchir silencieusement une fermeture applicative.
|
||||||
|
|
||||||
|
Les DTO Start/Stop refusent maintenant les champs JSON inconnus et bornent le seul identifiant libre `profile_id`. Le bridge de logging frontend refuse également les champs inconnus, borne niveau/target/message avant parsing et rejette les caractères de contrôle ; le frontend borne lui-même le message technique avant IPC avec une marge sûre vis-à-vis de l'encodage UTF-8. Les erreurs Tauri continuent à journaliser uniquement `domain/code`, jamais le payload hostile ni une ressource physique.
|
||||||
|
|
||||||
|
Aucun module de production supplémentaire, aucune dépendance lower-layer et aucune seconde pipeline ne sont introduits. Les comportements Config, Transport, Store, Worker et Common RAW restent inchangés.
|
||||||
|
|
||||||
Budget cible : une tranche hardening.
|
Budget cible : une tranche hardening.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
|
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
|
||||||
<!-- version: 21 -->
|
<!-- version: 22 -->
|
||||||
|
|
||||||
# Validation v0.3.15 — Raw Transaction Ingest Desk
|
# Validation v0.3.15 — Raw Transaction Ingest Desk
|
||||||
|
|
||||||
@@ -743,3 +743,55 @@ pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth
|
|||||||
```
|
```
|
||||||
|
|
||||||
Aucune modification Config, Transport, Store, Worker ou Common RAW n'est introduite par `pre.011`. Le module Rust de production du Desk reste exactement celui de `pre.010`; la tranche suivante `pre.012` reste dédiée aux races/shutdown/sécurité IPC.
|
Aucune modification Config, Transport, Store, Worker ou Common RAW n'est introduite par `pre.011`. Le module Rust de production du Desk reste exactement celui de `pre.010`; la tranche suivante `pre.012` reste dédiée aux races/shutdown/sécurité IPC.
|
||||||
|
|
||||||
|
### `pre.012` — races Start/Stop, shutdown applicatif et sécurité IPC
|
||||||
|
|
||||||
|
Base : `0.3.15-pre.011-fix.001`, validée par le gate opérateur du 16 septembre 2026. Les audits, `cargo check --workspace`, Clippy strict, les suites Desk/Worker/workspace et le scénario live multi-route passent ; Yellowstone et HTTP Block Polling restent manipulables simultanément sous charge et le Store partagé ferme seulement après la dernière route.
|
||||||
|
|
||||||
|
Périmètre `pre.012` : hardening du contrôle Desk uniquement, sans modification des stratégies d'acquisition.
|
||||||
|
|
||||||
|
```text
|
||||||
|
shutdown application
|
||||||
|
CloseRequested main window intercepté et empêché temporairement
|
||||||
|
admission de nouveaux Start fermée une seule fois
|
||||||
|
Stop coopératif demandé à tous les Workers actifs
|
||||||
|
réservations Starting marquées stop_requested
|
||||||
|
attente bornée routes + Store avant app_handle.exit
|
||||||
|
second CloseRequested n'engendre aucun second shutdown
|
||||||
|
|
||||||
|
race Start / Stop
|
||||||
|
Stop sur Starting => marque stop_requested puis attend le cleanup
|
||||||
|
activation ultérieure => request_stop avant acknowledgement
|
||||||
|
échec avant activation => rollback retient un terminal logique Stopped
|
||||||
|
cleanup Desk borné et compatible avec le drain Worker borné
|
||||||
|
|
||||||
|
stale inventory / Start
|
||||||
|
génération sous verrou pendant la préparation synchrone
|
||||||
|
reconstruction complète Config/Transport à chaque Start
|
||||||
|
verrou libéré explicitement avant le premier await
|
||||||
|
admission shutdown revérifiée atomiquement pendant reserve
|
||||||
|
|
||||||
|
IPC hostile
|
||||||
|
StartRequest / StopRequest : deny_unknown_fields
|
||||||
|
profile_id : non vide, <= 256 octets, sans trim implicite ni contrôle
|
||||||
|
FrontendLogPayload : deny_unknown_fields
|
||||||
|
level / target <= 16 octets
|
||||||
|
message <= 8192 octets et sans caractères de contrôle
|
||||||
|
frontend : message technique borné à 1024 code units avant IPC
|
||||||
|
erreurs Tauri : projection domain/code uniquement
|
||||||
|
```
|
||||||
|
|
||||||
|
Canaris ajoutés/étendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre_012_route_requests_reject_unknown_fields_and_bound_free_form_profile_identity
|
||||||
|
pre_012_frontend_log_ipc_rejects_unknown_fields_and_unbounded_or_control_messages
|
||||||
|
pre_012_stop_racing_start_marks_the_reservation_for_cooperative_stop
|
||||||
|
pre_012_application_shutdown_is_one_shot_and_marks_starting_routes_before_activation
|
||||||
|
pre_012_window_shutdown_and_start_stop_races_are_bounded_backend_owned
|
||||||
|
pre_012_hostile_ipc_is_shape_strict_bounded_and_safely_projected
|
||||||
|
pre_012_race_shutdown_and_ipc_hardening_adds_no_production_module_or_lower_layer_growth
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-claims : `pre.012` ne modifie ni Config, ni Transport, ni Store, ni `ksp-worker-raw-transaction-ingest-lib`, ni Common RAW. Il n'ajoute aucune orchestration Backfill, aucun scheduler, aucune route et aucune nouvelle donnée frontend. `pre.013` reste propriétaire de la fermeture completeness/security cross-layer.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user