0.3.15-pre.013
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 626
|
||||
# version: 627
|
||||
|
||||
[workspace]
|
||||
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"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.15-pre.12.fix.4"
|
||||
version = "0.3.15-pre.13"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-worker-raw-transaction-ingest-lib/README.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# ksp-worker-raw-transaction-ingest-lib
|
||||
|
||||
@@ -80,12 +80,14 @@ La construction est sans I/O et refuse notamment :
|
||||
- un commitment `Processed` ou implicite ;
|
||||
- un réseau Yellowstone non représentable ;
|
||||
- une provenance provider/endpoint non représentable ;
|
||||
- l'absence d'une route HTTP compatible pour `getTransaction` sur le même réseau.
|
||||
- l'absence d'une route HTTP compatible avec le mode retenu : `getTransaction` pour les signaux transactionnels, `getBlock` pour un abonnement Yellowstone Block pur, sur le même réseau.
|
||||
|
||||
Le runtime-resource aggregate public accepte une collection validée de 1 à 32 sources logiques et les démarre simultanément sous un supervisor privé. La collection est validée entièrement avant spawn ; aucun sous-ensemble silencieux, source primaire implicite ou standby n'est choisi. La collection interne, les `source_key`, les URLs, les filtres et les clients inférieurs ne sont pas exposés.
|
||||
|
||||
Le supervisor possède toutes les tâches source. Une perte de source n'est plus assimilée automatiquement à un fault : lorsqu'elle porte une plage de continuité bornée, le Worker l'inscrit dans son ledger run-local puis n'autorise la continuation des siblings que si la coverage passée de cette perte est réconciliée et si les sources encore actives couvrent explicitement tout le `TargetCoverage` futur. Une perte sans plage sûre, une coverage insuffisante ou un gap encore ouvert reste terminal. Le Worker ne respawn jamais lui-même une source Transport.
|
||||
|
||||
Pour un abonnement Yellowstone `Block` pur, la notification gRPC est un trigger de slot et ne bloque plus la boucle de réception pendant l'hydration HTTP. Le slot entre dans un coordinateur privé borné ; les `getBlock` puis l'admission complète du bloc s'exécutent dans au plus le quota `in-flight` attribué à cette source, tandis que le nombre de slots en attente reste borné par son quota `pending`. La boucle Yellowstone continue donc à consommer `next_update()` tant qu'une capacité pending existe. Aucun `unbounded_channel`, aucun task détaché et aucun silent drop n'est introduit ; si le débit aval reste durablement inférieur au débit de chaîne jusqu'à saturation de toutes les bornes, la politique reste fail-closed.
|
||||
|
||||
Un inventaire privé `source_key -> latest processing/source state`, borné à 32 entrées, agrège la projection run-local. La frontier agrégée reste conservative : elle n'expose un `processing_frontier_slot` que lorsque toutes les sources en possèdent un, choisit le minimum des frontiers connus et le plus ancien pending. Les sources reference-bearing partagent en plus un registre global d'hydration borné : une même clé `(network, signature, commitment)` ne déclenche qu'un leader HTTP, puis chaque signal source conserve sa propre provenance lors de la finalisation.
|
||||
|
||||
## Contrat de source Standard Logs + HTTP
|
||||
@@ -171,17 +173,17 @@ Le shutdown est borné par `shutdown_drain_timeout` (10 s par défaut). Le super
|
||||
|
||||
La queue centrale est un `tokio::sync::mpsc` privé borné par `admission_queue_capacity`. Les sources internes subissent la backpressure ; aucune queue non bornée ni silent drop n'est autorisé.
|
||||
|
||||
Le Worker possède des coordinateurs source-neutral Yellowstone, Standard Logs et Helius Transaction branchés sur un registre global d'hydration partagé. Standard Block et HTTP Block Polling n'entrent pas dans ce registre lorsqu'une transaction est direct-qualified.
|
||||
Le Worker possède des coordinateurs bornés pour Yellowstone, Standard Logs et Helius Transaction. Les voies Yellowstone/Standard Logs/Helius basées sur des références partagent le registre global d'hydration `(network, signature, commitment)`. Le mode Yellowstone Block utilise un coordinateur de slots séparé mais reçoit sa part des mêmes budgets globaux pending/in-flight avant spawn ; Standard Block et HTTP Block Polling n'entrent pas dans ces quotas lorsqu'une transaction est direct-qualified.
|
||||
|
||||
Les sources reference-bearing reçoivent des quotas déterministes dont la somme reste exactement dans les bornes techniques configurées :
|
||||
Les sources nécessitant une hydration HTTP asynchrone, y compris Yellowstone Block, reçoivent des quotas déterministes dont la somme reste exactement dans les bornes techniques configurées :
|
||||
|
||||
```text
|
||||
somme pending source signals <= admission_queue_capacity
|
||||
somme hydration tasks in flight <= persistence_concurrency
|
||||
source reference-bearing active => quota pending >= 1 et quota in-flight >= 1
|
||||
source hydratante active => quota pending >= 1 et quota in-flight >= 1
|
||||
```
|
||||
|
||||
Pour garantir simultanément ces bornes et l'absence de starvation structurelle, le démarrage échoue avant spawn si le nombre de sources reference-bearing dépasse `admission_queue_capacity` ou `persistence_concurrency`. Un sémaphore global protège en plus l'ouverture effective des hydrations HTTP.
|
||||
Pour garantir simultanément ces bornes et l'absence de starvation structurelle, le démarrage échoue avant spawn si le nombre de sources hydratantes dépasse `admission_queue_capacity` ou `persistence_concurrency`. Le registre des hydrations transactionnelles protège en plus l'ouverture effective des `getTransaction`; Yellowstone Block reste borné par sa partition déterministe et son `JoinSet` possédé.
|
||||
|
||||
Les signaux partageant le même `(network, signature, commitment)` sont coalescés cross-source avant le fan-out HTTP. La publication du résultat partagé notifie les followers sous le verrou de registry avant de retirer la clé : une nouvelle génération de leader ne peut donc pas s'intercaler entre retrait et notification. Après canonicalisation, une cache run-local bornée sérialise les acquisitions de même `(network, signature)` : la première passe par l'écriture atomique entity + observation, les suivantes de contenu canonique identique ajoutent uniquement leur observation déterministe. Une divergence de slot, block time, format ou hash canonique devient un content conflict terminal ; aucune majorité, préférence provider ou overwrite n'est appliqué. Le Store conserve son guard durable final.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
|
||||
// version: 47
|
||||
// version: 48
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
@@ -424,7 +424,7 @@ impl RawTransactionIngestLiveSource {
|
||||
fn uses_hydration(&self) -> bool {
|
||||
return match self {
|
||||
Self::HeliusTransaction(_) | Self::StandardLogs(_) => true,
|
||||
Self::Yellowstone(source) => source.uses_transaction_hydration(),
|
||||
Self::Yellowstone(_) => true,
|
||||
Self::HttpBlockPolling(_) | Self::StandardBlock(_) => false,
|
||||
};
|
||||
}
|
||||
@@ -1014,6 +1014,123 @@ struct RawTransactionIngestHydrationContext {
|
||||
source_key_domain: &'static [u8],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RawTransactionIngestYellowstoneBlockHydrationContext {
|
||||
capture_session: ksp_store_lib::RawProvenanceCode,
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment,
|
||||
http_pool: ksp_onchain_transport_lib::HttpTransportPool,
|
||||
hydration_role: ksp_onchain_transport_lib::HttpRoleName,
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
route: RawTransactionIngestSourceRoute,
|
||||
source_key: [u8; 32],
|
||||
}
|
||||
|
||||
type RawTransactionIngestYellowstoneBlockHydrationTasks = tokio::task::JoinSet<ksp_core_lib::Result<std::option::Option<u64>>>;
|
||||
|
||||
struct RawTransactionIngestYellowstoneBlockHydrationCoordinator {
|
||||
max_in_flight: usize,
|
||||
max_pending_slots: usize,
|
||||
pending: std::collections::BTreeMap<u64, bool>,
|
||||
tasks: RawTransactionIngestYellowstoneBlockHydrationTasks,
|
||||
}
|
||||
|
||||
impl RawTransactionIngestYellowstoneBlockHydrationCoordinator {
|
||||
fn new(max_pending_slots: usize, max_in_flight: usize) -> Self {
|
||||
return Self {
|
||||
max_in_flight,
|
||||
max_pending_slots,
|
||||
pending: std::collections::BTreeMap::new(),
|
||||
tasks: RawTransactionIngestYellowstoneBlockHydrationTasks::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn can_receive(&self) -> bool {
|
||||
return self.pending.len() < self.max_pending_slots;
|
||||
}
|
||||
|
||||
fn queue_slot(&mut self, slot: u64, processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter) -> ksp_core_lib::Result<()> {
|
||||
if self.pending.contains_key(&slot) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
if self.max_pending_slots == 0 || self.pending.len() >= self.max_pending_slots {
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_hydration_pending_saturated"));
|
||||
}
|
||||
if let std::result::Result::Err(error) = processing_frontier.observe_pending(slot) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let _previous = self.pending.insert(slot, false);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn start_hydrations(
|
||||
&mut self,
|
||||
hydration: &RawTransactionIngestYellowstoneBlockHydrationContext,
|
||||
admission_sender: &tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
stop_receiver: &tokio::sync::watch::Receiver<bool>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if self.max_in_flight == 0 {
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_hydration_concurrency_missing"));
|
||||
}
|
||||
while self.tasks.len() < self.max_in_flight {
|
||||
let slot = self.pending.iter().find_map(|(slot, in_flight)| {
|
||||
if *in_flight {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return std::option::Option::Some(*slot);
|
||||
});
|
||||
let slot = match slot {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => break,
|
||||
};
|
||||
let in_flight = match self.pending.get_mut(&slot) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_hydration_pending_missing"));
|
||||
},
|
||||
};
|
||||
*in_flight = true;
|
||||
let task_hydration = hydration.clone();
|
||||
let task_admission_sender = admission_sender.clone();
|
||||
let task_stop_receiver = stop_receiver.clone();
|
||||
let _abort_handle = self.tasks.spawn(async move {
|
||||
return hydrate_yellowstone_block(task_hydration, slot, task_admission_sender, task_stop_receiver).await;
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn handle_joined(
|
||||
&mut self,
|
||||
joined: std::result::Result<ksp_core_lib::Result<std::option::Option<u64>>, tokio::task::JoinError>,
|
||||
processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter,
|
||||
) -> ksp_core_lib::Result<bool> {
|
||||
let completed_slot = match joined {
|
||||
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
|
||||
std::result::Result::Ok(std::result::Result::Err(error)) => return std::result::Result::Err(error),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_hydration_task_join_failed")),
|
||||
};
|
||||
let slot = match completed_slot {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Ok(false),
|
||||
};
|
||||
if self.pending.remove(&slot).is_none() {
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_hydration_result_without_pending"));
|
||||
}
|
||||
if let std::result::Result::Err(error) = processing_frontier.settle_pending(slot) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(true);
|
||||
}
|
||||
|
||||
async fn abort_all(&mut self, processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter) {
|
||||
self.tasks.abort_all();
|
||||
while self.tasks.join_next().await.is_some() {}
|
||||
self.pending.clear();
|
||||
processing_frontier.discard_all_pending();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum RawTransactionIngestYellowstoneMode {
|
||||
TransactionHydration,
|
||||
@@ -1101,10 +1218,6 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
return std::result::Result::Ok(Self { yellowstone_channel, subscribe_request, http_pool, hydration_role, mode, network, route, source_key });
|
||||
}
|
||||
|
||||
fn uses_transaction_hydration(&self) -> bool {
|
||||
return self.mode == RawTransactionIngestYellowstoneMode::TransactionHydration;
|
||||
}
|
||||
|
||||
fn hydration_context(&self) -> RawTransactionIngestHydrationContext {
|
||||
let commitment = match self.subscribe_request.commitment() {
|
||||
std::option::Option::Some(value) => value,
|
||||
@@ -1123,6 +1236,31 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
};
|
||||
}
|
||||
|
||||
fn block_hydration_context(
|
||||
&self,
|
||||
settings: &crate::RawTransactionIngestSettings,
|
||||
) -> ksp_core_lib::Result<RawTransactionIngestYellowstoneBlockHydrationContext> {
|
||||
let commitment = match self.subscribe_request.commitment() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.yellowstone_commitment_missing")),
|
||||
};
|
||||
let capture_session = match ksp_store_lib::RawProvenanceCode::new(settings.worker_id().as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_capture_session_unrepresentable"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(RawTransactionIngestYellowstoneBlockHydrationContext {
|
||||
capture_session,
|
||||
commitment,
|
||||
http_pool: self.http_pool.clone(),
|
||||
hydration_role: self.hydration_role.clone(),
|
||||
network: self.network.clone(),
|
||||
route: self.route.clone(),
|
||||
source_key: self.source_key,
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs the productive Yellowstone source task until cooperative stop or one safe terminal source failure.
|
||||
async fn run(
|
||||
self,
|
||||
@@ -1133,7 +1271,7 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
shared: RawTransactionIngestSourceRuntimeShared,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if self.mode == RawTransactionIngestYellowstoneMode::BlockHydration {
|
||||
return self.run_block_hydration(settings, stop_receiver, admission_sender, processing_frontier_sender).await;
|
||||
return self.run_block_hydration(settings, stop_receiver, admission_sender, processing_frontier_sender, shared).await;
|
||||
}
|
||||
let RawTransactionIngestSourceRuntimeShared { continuity_contracts, global_hydration_registry, hydration_in_flight_limit, hydration_pending_limit } =
|
||||
shared;
|
||||
@@ -1269,7 +1407,13 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
mut stop_receiver: tokio::sync::watch::Receiver<bool>,
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
|
||||
shared: RawTransactionIngestSourceRuntimeShared,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let RawTransactionIngestSourceRuntimeShared { hydration_in_flight_limit, hydration_pending_limit, .. } = shared;
|
||||
let hydration = match self.block_hydration_context(&settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let opened = tokio::select! {
|
||||
biased;
|
||||
_ = stop_receiver.changed() => {
|
||||
@@ -1281,6 +1425,7 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(source_transport_error(error.code())),
|
||||
};
|
||||
let mut coordinator = RawTransactionIngestYellowstoneBlockHydrationCoordinator::new(hydration_pending_limit, hydration_in_flight_limit);
|
||||
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
|
||||
let snapshot_source = session.snapshot_source();
|
||||
if let std::result::Result::Err(error) = processing_frontier.observe_session_snapshot(snapshot_source.current()) {
|
||||
@@ -1294,6 +1439,15 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
if *stop_receiver.borrow() {
|
||||
break;
|
||||
}
|
||||
if let std::result::Result::Err(error) = coordinator.start_hydrations(&hydration, &admission_sender, &stop_receiver) {
|
||||
fault = std::option::Option::Some(error);
|
||||
break;
|
||||
}
|
||||
let can_receive = coordinator.can_receive();
|
||||
if !can_receive && coordinator.tasks.is_empty() {
|
||||
fault = std::option::Option::Some(crate::runtime_error("source.yellowstone_block_hydration_stalled"));
|
||||
break;
|
||||
}
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = stop_receiver.changed() => {
|
||||
@@ -1310,7 +1464,24 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
std::option::Option::None => session_snapshot_source = std::option::Option::None,
|
||||
}
|
||||
}
|
||||
update = session.next_update() => {
|
||||
joined = coordinator.tasks.join_next(), if !coordinator.tasks.is_empty() => {
|
||||
let joined = match joined {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
fault = std::option::Option::Some(crate::runtime_error("source.yellowstone_block_hydration_join_missing"));
|
||||
break;
|
||||
},
|
||||
};
|
||||
match coordinator.handle_joined(joined, &mut processing_frontier) {
|
||||
std::result::Result::Ok(true) => {},
|
||||
std::result::Result::Ok(false) => break,
|
||||
std::result::Result::Err(error) => {
|
||||
fault = std::option::Option::Some(error);
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
update = session.next_update(), if can_receive => {
|
||||
let update = match update {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
@@ -1322,42 +1493,16 @@ impl crate::RawTransactionIngestYellowstoneSource {
|
||||
break;
|
||||
},
|
||||
};
|
||||
if let ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) = update {
|
||||
let fetched = fetch_yellowstone_block_ingresses(&self, &settings, value.slot(), &mut stop_receiver).await;
|
||||
let ingresses = match fetched {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => break,
|
||||
std::result::Result::Err(error) => {
|
||||
fault = std::option::Option::Some(error);
|
||||
break;
|
||||
},
|
||||
};
|
||||
for ingress in ingresses {
|
||||
let sent = tokio::select! {
|
||||
biased;
|
||||
_ = stop_receiver.changed() => {
|
||||
break;
|
||||
}
|
||||
result = admission_sender.send(ingress) => result,
|
||||
};
|
||||
if sent.is_err() {
|
||||
if !*stop_receiver.borrow() {
|
||||
fault = std::option::Option::Some(crate::runtime_error("source.admission_closed"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if fault.is_some() || *stop_receiver.borrow() {
|
||||
break;
|
||||
}
|
||||
if let std::result::Result::Err(error) = processing_frontier.observe_settled(value.slot()) {
|
||||
fault = std::option::Option::Some(error);
|
||||
break;
|
||||
}
|
||||
if let ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) = update
|
||||
&& let std::result::Result::Err(error) = coordinator.queue_slot(value.slot(), &mut processing_frontier)
|
||||
{
|
||||
fault = std::option::Option::Some(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinator.abort_all(&mut processing_frontier).await;
|
||||
processing_frontier.set_source_state(crate::RawTransactionIngestSourceState::Closing);
|
||||
let closed = session.close().await;
|
||||
if let std::option::Option::Some(error) = fault {
|
||||
@@ -3376,16 +3521,42 @@ fn validate_http_block_polling_discovery(next_scan_slot: u64, discovered: &[u64]
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
async fn hydrate_yellowstone_block(
|
||||
hydration: RawTransactionIngestYellowstoneBlockHydrationContext,
|
||||
slot: u64,
|
||||
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
|
||||
mut stop_receiver: tokio::sync::watch::Receiver<bool>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
||||
let fetched = fetch_yellowstone_block_ingresses(&hydration, slot, &mut stop_receiver).await;
|
||||
let ingresses = match fetched {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Ok(std::option::Option::None),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
for ingress in ingresses {
|
||||
let sent = tokio::select! {
|
||||
biased;
|
||||
_ = stop_receiver.changed() => {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
result = admission_sender.send(ingress) => result,
|
||||
};
|
||||
if sent.is_err() {
|
||||
if *stop_receiver.borrow() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
return std::result::Result::Err(crate::runtime_error("source.admission_closed"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::Some(slot));
|
||||
}
|
||||
|
||||
async fn fetch_yellowstone_block_ingresses(
|
||||
source: &crate::RawTransactionIngestYellowstoneSource,
|
||||
settings: &crate::RawTransactionIngestSettings,
|
||||
hydration: &RawTransactionIngestYellowstoneBlockHydrationContext,
|
||||
slot: u64,
|
||||
stop_receiver: &mut tokio::sync::watch::Receiver<bool>,
|
||||
) -> ksp_core_lib::Result<std::option::Option<std::vec::Vec<crate::RawTransactionIngress>>> {
|
||||
let commitment = match source.subscribe_request.commitment() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.yellowstone_commitment_missing")),
|
||||
};
|
||||
let commitment = hydration.commitment;
|
||||
let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(commitment),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||
@@ -3401,7 +3572,7 @@ async fn fetch_yellowstone_block_ingresses(
|
||||
_ = stop_receiver.changed() => {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
result = source.http_pool.get_block_observed(&source.hydration_role, slot, std::option::Option::Some(&config)) => result,
|
||||
result = hydration.http_pool.get_block_observed(&hydration.hydration_role, slot, std::option::Option::Some(&config)) => result,
|
||||
};
|
||||
match request {
|
||||
std::result::Result::Ok(value) if value.value().is_some() => break value,
|
||||
@@ -3434,36 +3605,35 @@ async fn fetch_yellowstone_block_ingresses(
|
||||
return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_transactions_missing"));
|
||||
},
|
||||
};
|
||||
let provenance = build_yellowstone_block_provenance(source, settings, observed.endpoint_name(), observed.provider(), received_at);
|
||||
let provenance = build_yellowstone_block_provenance(hydration, observed.endpoint_name(), observed.provider(), received_at);
|
||||
let provenance = match provenance {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut ingresses = std::vec::Vec::with_capacity(transactions.len());
|
||||
for (position, transaction) in transactions.iter().enumerate() {
|
||||
let material = build_http_block_polling_material_from_transaction(&source.network, slot, block.block_time(), transaction, position);
|
||||
let material = build_http_block_polling_material_from_transaction(&hydration.network, slot, block.block_time(), transaction, position);
|
||||
let material = match material {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ingresses.push(crate::RawTransactionIngress {
|
||||
material,
|
||||
network: source.network.clone(),
|
||||
network: hydration.network.clone(),
|
||||
provenance: provenance.clone(),
|
||||
source_key: source.source_key,
|
||||
source_key: hydration.source_key,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(std::option::Option::Some(ingresses));
|
||||
}
|
||||
|
||||
fn build_yellowstone_block_provenance(
|
||||
source: &crate::RawTransactionIngestYellowstoneSource,
|
||||
settings: &crate::RawTransactionIngestSettings,
|
||||
hydration: &RawTransactionIngestYellowstoneBlockHydrationContext,
|
||||
endpoint_name: &str,
|
||||
provider_name: &ksp_onchain_transport_lib::HttpProviderName,
|
||||
received_at: ksp_store_lib::RawTimestamp,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAcquisitionProvenance> {
|
||||
let (provider, endpoint_id) = match composite_provenance_codes(&source.route, "ys", provider_name.as_str(), endpoint_name) {
|
||||
let (provider, endpoint_id) = match composite_provenance_codes(&hydration.route, "ys", provider_name.as_str(), endpoint_name) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -3475,15 +3645,7 @@ fn build_yellowstone_block_provenance(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_method_unrepresentable")),
|
||||
};
|
||||
let capture_session = match ksp_store_lib::RawProvenanceCode::new(settings.worker_id().as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_capture_session_unrepresentable")),
|
||||
};
|
||||
let commitment = match source.subscribe_request.commitment() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.yellowstone_commitment_missing")),
|
||||
};
|
||||
let commitment = match ksp_store_lib::RawProvenanceCode::new(commitment.as_str()) {
|
||||
let commitment = match ksp_store_lib::RawProvenanceCode::new(hydration.commitment.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("source.yellowstone_block_commitment_unrepresentable")),
|
||||
};
|
||||
@@ -3491,7 +3653,7 @@ fn build_yellowstone_block_provenance(
|
||||
ksp_store_lib::RawAcquisitionProvenance::new(provider, protocol, acquisition_method, ksp_store_lib::RawAcquisitionOrigin::Live, received_at)
|
||||
.with_endpoint_id(endpoint_id)
|
||||
.with_commitment(commitment)
|
||||
.with_capture_session_id(capture_session),
|
||||
.with_capture_session_id(hydration.capture_session.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs
|
||||
// version: 38
|
||||
// version: 39
|
||||
|
||||
//! External public, security, redaction and release-boundary hardening canaries through `v0.3.15-pre.004`.
|
||||
|
||||
@@ -1413,3 +1413,39 @@ fn v0_3_15_pre_004_websocket_source_constructors_enforce_transport_capabilities_
|
||||
assert!(!resources.contains("match ws_endpoint.provider()"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_013_yellowstone_block_hydration_is_bounded_concurrent_and_stop_preemptible() {
|
||||
let resources = include_str!("../src/runtime_resources.rs");
|
||||
let block_run = match resources.split_once("async fn run_block_hydration(") {
|
||||
std::option::Option::Some((_, tail)) => match tail.split_once("/// Validated Helius") {
|
||||
std::option::Option::Some((body, _)) => body,
|
||||
std::option::Option::None => tail,
|
||||
},
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
for required in [
|
||||
"RawTransactionIngestYellowstoneBlockHydrationCoordinator::new",
|
||||
"coordinator.start_hydrations",
|
||||
"joined = coordinator.tasks.join_next()",
|
||||
"update = session.next_update(), if can_receive",
|
||||
"coordinator.queue_slot(value.slot()",
|
||||
"coordinator.abort_all(&mut processing_frontier).await",
|
||||
] {
|
||||
assert!(block_run.contains(required), "required pre.013 bounded Yellowstone block hydration guard missing: {required}");
|
||||
}
|
||||
assert!(!block_run.contains("fetch_yellowstone_block_ingresses(&self"));
|
||||
assert!(!block_run.contains("admission_sender.send(ingress)"));
|
||||
for required in [
|
||||
"hydrate_yellowstone_block",
|
||||
"result = admission_sender.send(ingress)",
|
||||
"source.yellowstone_block_hydration_pending_saturated",
|
||||
"source.yellowstone_block_hydration_concurrency_missing",
|
||||
] {
|
||||
assert!(resources.contains(required), "required pre.013 Yellowstone block hydration bound missing: {required}");
|
||||
}
|
||||
for forbidden in ["unbounded_channel", "tokio::spawn(", "std::thread::spawn("] {
|
||||
assert!(!block_run.contains(forbidden), "pre.013 introduced forbidden unbounded/detached execution: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs
|
||||
// version: 34
|
||||
// version: 35
|
||||
|
||||
//! Release-completeness canaries through the `v0.3.15-pre.004` WebSocket capability-enforcement tranche.
|
||||
|
||||
@@ -484,3 +484,15 @@ fn v0_3_15_pre_004_websocket_capability_enforcement_canaries_are_complete_withou
|
||||
assert!(!root.contains("supports_subscription"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_013_yellowstone_block_backpressure_fix_adds_no_public_surface() {
|
||||
let resource_tests = include_str!("../unit_tests/runtime_resources.rs");
|
||||
let hardening = include_str!("hardening.rs");
|
||||
let root = include_str!("../src/lib.rs");
|
||||
assert!(resource_tests.contains("v0_3_15_pre_013_yellowstone_block_hydration_coordinator_is_bounded_before_http_work"));
|
||||
assert!(hardening.contains("v0_3_15_pre_013_yellowstone_block_hydration_is_bounded_concurrent_and_stop_preemptible"));
|
||||
assert!(!root.contains("YellowstoneBlockHydrationCoordinator"));
|
||||
assert!(!root.contains("YellowstoneBlockHydrationContext"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs
|
||||
// version: 38
|
||||
// version: 39
|
||||
|
||||
fn grpc_endpoint(cluster: &str) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||
return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider");
|
||||
@@ -879,7 +879,31 @@ async fn pre_009_yellowstone_block_mode_uses_get_block_without_transaction_hydra
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(source.mode, super::RawTransactionIngestYellowstoneMode::BlockHydration);
|
||||
assert!(!source.uses_transaction_hydration());
|
||||
assert!(super::RawTransactionIngestLiveSource::Yellowstone(source).uses_hydration());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_15_pre_013_yellowstone_block_hydration_coordinator_is_bounded_before_http_work() {
|
||||
let (sender, receiver) = tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
||||
let mut processing_frontier = super::RawTransactionIngestProcessingFrontierReporter::new(sender);
|
||||
let mut coordinator = super::RawTransactionIngestYellowstoneBlockHydrationCoordinator::new(2, 1);
|
||||
assert!(coordinator.can_receive());
|
||||
assert!(coordinator.queue_slot(100, &mut processing_frontier).is_ok());
|
||||
assert!(coordinator.queue_slot(101, &mut processing_frontier).is_ok());
|
||||
assert!(!coordinator.can_receive());
|
||||
assert_eq!(receiver.borrow().hydration_pending(), 2);
|
||||
assert_eq!(receiver.borrow().oldest_pending_slot(), std::option::Option::Some(100));
|
||||
let saturated = coordinator.queue_slot(102, &mut processing_frontier);
|
||||
let error = match saturated {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
assert_eq!(error.context()[0].value(), "source.yellowstone_block_hydration_pending_saturated");
|
||||
assert_eq!(coordinator.pending.len(), 2);
|
||||
assert_eq!(coordinator.tasks.len(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
|
||||
169
deltas/0.3.15/pre.013.md
Normal file
169
deltas/0.3.15/pre.013.md
Normal file
@@ -0,0 +1,169 @@
|
||||
<!-- file: deltas/0.3.15/pre.013.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta v0.3.15-pre.013 — Yellowstone Block hydration backpressure
|
||||
|
||||
## Base
|
||||
|
||||
```text
|
||||
ksp-general-0.3.15-pre.012-fix.004.zip
|
||||
SHA-256: f88de2b95c2aab48043fd64b7825d9c508dc38d8f6ef51247ebce0da31be810e
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.012-fix.004` compile et teste proprement les nouveaux diagnostics Store/Worker. Le live Mainnet ne reproduit aucun `content_conflict`, mais Yellowstone devient deux fois terminal `source_failed / onchain_transport.grpc_backpressure_overflow`.
|
||||
|
||||
Les WARN Transport apparaissent avant les Stop opérateur :
|
||||
|
||||
```text
|
||||
22:06:52.734796 Yellowstone subscribe update queue overflowed
|
||||
22:07:10.781900 Stop Yellowstone demandé
|
||||
|
||||
22:08:39.746262 Yellowstone subscribe update queue overflowed
|
||||
22:08:51.337483 Stop Yellowstone demandé
|
||||
```
|
||||
|
||||
Le défaut n'appartient donc ni au Stop Desk ni au diagnostic `pre.012-fix.004`. Il appartient au couloir acquisition Yellowstone antérieur. Conformément à `VER-LIFECYCLE-010`, il ouvre une nouvelle prerelease dédiée et ne devient pas `pre.012-fix.005`.
|
||||
|
||||
## Cause
|
||||
|
||||
Le mode Yellowstone Block était la seule voie d'hydration HTTP exécutée séquentiellement dans la boucle de réception source :
|
||||
|
||||
```text
|
||||
Block update gRPC
|
||||
-> await HTTP getBlock
|
||||
-> await admission de toutes les transactions du bloc
|
||||
-> next_update gRPC suivant
|
||||
```
|
||||
|
||||
Pendant ces attentes, le Worker ne vidait plus la queue d'updates Transport. Sur Mainnet, la queue bornée finissait donc par atteindre sa capacité et Transport terminait correctement fail-closed avec `grpc_backpressure_overflow`.
|
||||
|
||||
Le mode Block était également exclu de `RawTransactionIngestLiveSource::uses_hydration()`, donc il ne recevait aucun quota `pending/in-flight` lors du partitionnement déjà utilisé par les sources à hydration HTTP.
|
||||
|
||||
## Correction
|
||||
|
||||
`RawTransactionIngestLiveSource::Yellowstone` participe désormais toujours au budget d'hydration, y compris en mode `BlockHydration`.
|
||||
|
||||
Le nouveau coordinateur privé `RawTransactionIngestYellowstoneBlockHydrationCoordinator` :
|
||||
|
||||
```text
|
||||
pending slots <= quota pending attribué à la source
|
||||
getBlock/admission tasks in flight <= quota in-flight attribué à la source
|
||||
JoinSet possédé par la source
|
||||
aucune queue non bornée
|
||||
aucun task détaché
|
||||
aucun silent drop
|
||||
```
|
||||
|
||||
La notification Block gRPC ne lance plus directement un `await getBlock` dans la branche `session.next_update()`. Elle réserve le slot dans le coordinateur. Jusqu'au quota in-flight, des tâches possédées exécutent en parallèle :
|
||||
|
||||
```text
|
||||
HTTP getBlock Full/Base64
|
||||
-> construction Common RAW existante
|
||||
-> admission complète de toutes les transactions du bloc
|
||||
-> retour du slot terminé
|
||||
```
|
||||
|
||||
Le slot n'est `settled` dans la processing frontier qu'après admission complète du bloc. Un Stop ou abort annule/joint les tâches et purge les pending sans settlement artificiel.
|
||||
|
||||
La boucle Yellowstone continue à consommer `session.next_update()` tant que le quota pending possède une place. Si le débit aval reste durablement inférieur au débit de chaîne jusqu'à saturation de toutes les bornes, la politique fail-closed reste inchangée : aucun agrandissement de la queue Transport ne masque la surcharge.
|
||||
|
||||
## Frontières
|
||||
|
||||
Aucun changement de :
|
||||
|
||||
```text
|
||||
YellowstoneGrpcSessionSettings.update_channel_capacity
|
||||
Transport gRPC actor
|
||||
Store / schema / RAW canonical identity
|
||||
content_conflict policy / diagnostics pre.012-fix.004
|
||||
Config
|
||||
Desk Start/Stop/shutdown/IPC
|
||||
HTTP Block Polling
|
||||
Standard Block / Standard Logs / Helius
|
||||
Backfill
|
||||
public Worker API
|
||||
```
|
||||
|
||||
Le correctif réutilise uniquement les budgets `admission_queue_capacity` et `persistence_concurrency` déjà possédés par le Worker pour partitionner les sources hydratantes.
|
||||
|
||||
## Canaris
|
||||
|
||||
Ajouts/évolutions :
|
||||
|
||||
```text
|
||||
v0_3_15_pre_013_yellowstone_block_hydration_coordinator_is_bounded_before_http_work
|
||||
v0_3_15_pre_013_yellowstone_block_hydration_is_bounded_concurrent_and_stop_preemptible
|
||||
v0_3_15_pre_013_yellowstone_block_backpressure_fix_adds_no_public_surface
|
||||
```
|
||||
|
||||
Le test historique `pre_009_yellowstone_block_mode_uses_get_block_without_transaction_hydration` vérifie maintenant aussi que le mode Block participe au budget d'hydration sans devenir une hydration `getTransaction`.
|
||||
|
||||
## Plan
|
||||
|
||||
Le forecast est décalé d'une prerelease :
|
||||
|
||||
```text
|
||||
pre.013 Yellowstone Block hydration backpressure
|
||||
pre.014 completeness/security cross-layer
|
||||
pre.015 gate technique/live final
|
||||
pre.016 réconciliation documentaire finale
|
||||
pre.017 préparation publication
|
||||
rel.001 stable mécanique
|
||||
```
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
workspace.package.version : 0.3.15-pre.13
|
||||
root Cargo header counter : 627
|
||||
```
|
||||
|
||||
## Validation attendue
|
||||
|
||||
```text
|
||||
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-worker-raw-transaction-ingest-lib --all-targets --all-features
|
||||
cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features
|
||||
cargo test --workspace --all-targets --all-features
|
||||
(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Live obligatoire :
|
||||
|
||||
```text
|
||||
1. Yellowstone Mainnet seul pendant au moins plusieurs minutes, donc au-delà des ~80 s de reproduction.
|
||||
2. Vérifier absence de grpc_backpressure_overflow et état Healthy/Running.
|
||||
3. Stop ciblé Yellowstone => Stopped/Healthy.
|
||||
4. Yellowstone + HTTP Block Polling en parallèle pendant plusieurs minutes.
|
||||
5. Vérifier les deux routes productives et manipulables.
|
||||
6. Vérifier absence de grpc_backpressure_overflow.
|
||||
7. Si content_conflict réapparaît, exploiter le diagnostic sémantique de pre.012-fix.004 sans le masquer.
|
||||
```
|
||||
|
||||
## Inventaire exact du delta
|
||||
|
||||
Ajout :
|
||||
|
||||
```text
|
||||
deltas/0.3.15/pre.013.md
|
||||
```
|
||||
|
||||
Modifications :
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/README.md
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/tests/hardening.rs
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/tests/release_completeness.rs
|
||||
crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.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 -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 21 -->
|
||||
|
||||
# Plan v0.3.15 — Raw Transaction Ingest Desk
|
||||
|
||||
@@ -342,7 +342,7 @@ La documentation Helius actuelle indique que `blockSubscribe` n'est pas support
|
||||
| Store open / network / partage `Arc<Store>` | aucun gap | Store + app | `ksp-store-lib` suffit |
|
||||
| Lifecycle / stop / snapshots Worker | aucun gap | Worker + app | handles publics existants |
|
||||
| Backfill / historical repair | hors scope | Job Backfill | aucune orchestration automatique |
|
||||
| Taille release | pas de split de release | plan | forecast étendu jusqu’à `pre.016` |
|
||||
| Taille release | pas de split de release | plan | forecast étendu jusqu’à `pre.017` |
|
||||
|
||||
La release n'est pas rescindée. Le gap WS est petit, clairement propriétaire et testable. En revanche, le forecast doit être étendu pour ne pas fusionner Transport, Config, Worker et scaffold UI dans une même tranche.
|
||||
|
||||
@@ -668,7 +668,7 @@ Le build Tauri final reste un gate tardif et ne remplace pas les tests runtime.
|
||||
|
||||
## 20. Forecast recalibré — prereleases souples
|
||||
|
||||
Le forecast initial `pre.002 -> pre.013` est étendu jusqu'à `pre.016`. La cause n'est pas une inflation UI : c'est la découverte d'un contrat WS capability manquant qui doit être fermé proprement dans Transport, Config puis Worker avant consommation par la Desk.
|
||||
Le forecast initial `pre.002 -> pre.013` est désormais étendu jusqu'à `pre.017`. La cause n'est pas une inflation UI : c'est la découverte d'un contrat WS capability manquant qui doit être fermé proprement dans Transport, Config puis Worker avant consommation par la Desk.
|
||||
|
||||
Le forecast reste volontairement souple. Chaque prerelease possède une sous-section directement éditable ; lorsqu'un correctif est nécessaire, il est ajouté sous la prerelease concernée avec un titre `#### pre.NNN-fix.MMM`. Les responsabilités de clôture `gate technique/live -> réconciliation documentaire -> préparation de publication` restent séparées.
|
||||
|
||||
@@ -847,25 +847,33 @@ Aucun module de production supplémentaire, aucune dépendance lower-layer et au
|
||||
|
||||
Budget cible : une tranche hardening.
|
||||
|
||||
### `pre.013` — completeness/security cross-layer
|
||||
### `pre.013` — Yellowstone Block hydration backpressure
|
||||
|
||||
**État : implémenté, gate opérateur requis.** Le live de `pre.012-fix.004` a reproduit deux `grpc_backpressure_overflow` Mainnet environ 79 s puis 81 s après démarrage Yellowstone. Les WARN Transport précèdent les Stop opérateur de plusieurs secondes : la cause est donc le couplage historique `Block update -> getBlock HTTP -> admission de toutes les transactions -> lecture de l'update suivante`, et non la sémantique Stop. Conformément à `VER-LIFECYCLE-010`, ce défaut du couloir acquisition antérieur ouvre une nouvelle prerelease dédiée au lieu d'un `pre.012-fix.005`.
|
||||
|
||||
Le mode Yellowstone Block reçoit désormais une partition déterministe des budgets d'hydration déjà possédés par le Worker. Les slots gRPC sont placés dans un coordinateur privé borné ; plusieurs `getBlock` peuvent progresser concurremment jusqu'au quota `in-flight`, et chaque tâche porte aussi l'admission complète de son bloc avant settlement du slot. La boucle source continue de consommer `session.next_update()` tant qu'une capacité pending existe. Aucun changement de capacité Transport, aucune queue non bornée et aucun drop silencieux ne masquent une surcharge durable.
|
||||
|
||||
Budget cible : une tranche corrective acquisition/throughput, suivie d'un live Mainnet prolongé.
|
||||
|
||||
### `pre.014` — completeness/security cross-layer
|
||||
|
||||
Fermer les canaris Config/Transport/Worker/Common RAW/Store/Desk, dependency firewalls, API publique, redaction et absence de seconde pipeline ou d'orchestration Backfill.
|
||||
|
||||
Budget cible : une tranche de clôture fonctionnelle.
|
||||
|
||||
### `pre.014` — gate technique/live final
|
||||
### `pre.015` — gate technique/live final
|
||||
|
||||
Exécuter le workspace complet, Clippy strict, suites ciblées, graphes/duplicates, smokes réellement accessibles et build frontend/Tauri final. Aucun nouveau scope métier.
|
||||
|
||||
Budget cible : une tranche technique.
|
||||
|
||||
### `pre.015` — réconciliation documentaire finale
|
||||
### `pre.016` — réconciliation documentaire finale
|
||||
|
||||
Réconcilier plan, validation, README/USAGE et documents durables réellement affectés. Ne pas modifier CHANGELOG, ROADMAP ni le prompt suivant.
|
||||
|
||||
Budget cible : une tranche documentaire.
|
||||
|
||||
### `pre.016` — préparation de publication
|
||||
### `pre.017` — préparation de publication
|
||||
|
||||
Préparer le prompt suivant, CHANGELOG, ROADMAP et la mécanique minimale de version/delta. Aucun rattrapage fonctionnel ou documentaire durable.
|
||||
|
||||
@@ -908,7 +916,7 @@ aucun gap Store / HTTP / lifecycle Worker essentiel
|
||||
capability WS fine = gap lower-layer réel à fermer avant UI
|
||||
Helius standard WS != Helius transactionSubscribe
|
||||
aucune route rendue disponible à partir d'un exemple Config ou du provider name
|
||||
forecast recalibré jusqu'à pre.016
|
||||
forecast recalibré jusqu'à pre.017
|
||||
```
|
||||
|
||||
`pre.002` doit donc commencer par le contrat Transport de capabilities WS, et non par le scaffold Tauri.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
|
||||
<!-- version: 26 -->
|
||||
<!-- version: 27 -->
|
||||
|
||||
# Validation v0.3.15 — Raw Transaction Ingest Desk
|
||||
|
||||
@@ -461,19 +461,23 @@ Ce correctif ne modifie ni Config, ni Transport, ni Store, ni Worker, ni Common
|
||||
|
||||
Races, shutdown app, sécurité IPC, hardening hostile/stale inventory.
|
||||
|
||||
### `pre.013` — completeness
|
||||
### `pre.013` — Yellowstone Block hydration backpressure
|
||||
|
||||
Découplage borné réception gRPC / `getBlock` / admission pour la route Yellowstone Block, conformément à `VER-LIFECYCLE-010`.
|
||||
|
||||
### `pre.014` — completeness
|
||||
|
||||
Cross-layer completeness + canaris système sans expansion de frontières.
|
||||
|
||||
### `pre.014` — gate technique/live
|
||||
### `pre.015` — gate technique/live
|
||||
|
||||
Gate technique/live accessible + frontend/Tauri + build final de validation technique.
|
||||
|
||||
### `pre.015` — réconciliation documentaire
|
||||
### `pre.016` — réconciliation documentaire
|
||||
|
||||
Plan/validation/README/USAGE uniquement selon les surfaces réellement affectées.
|
||||
|
||||
### `pre.016` — préparation publication
|
||||
### `pre.017` — préparation publication
|
||||
|
||||
Prompt suivant + CHANGELOG + ROADMAP + mécanique minimale de publication.
|
||||
|
||||
@@ -793,7 +797,7 @@ 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.
|
||||
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.014` reste propriétaire de la fermeture completeness/security cross-layer.
|
||||
### `pre.012-fix.001` — future Tauri Start `Send`
|
||||
|
||||
Le gate opérateur de `pre.012` a arrêté la tranche sur un défaut de compilation déterministe dans `AppState::start_route` : le `std::sync::MutexGuard<u32>` de `inventory_generation` restait considéré vivant au franchissement du `await` final, malgré `std::mem::drop(generation)`. Le futur exposé par la commande Tauri `start_route` ne satisfaisait donc plus la borne `Send` imposée par `tauri::ipc::ResultFutureTag::future`. Les audits Rust/Markdown étaient propres et les suites Worker indépendantes restaient vertes, mais `cargo check`, Clippy, les tests Desk/workspace et `cargo tauri dev` ne pouvaient pas compiler le Desk.
|
||||
@@ -850,4 +854,34 @@ Une seconde ligne de diagnostic lit au mieux l'observation durable la plus ancie
|
||||
Ce fix n'altère ni l'identité canonique, ni la comparaison d'égalité, ni la politique de conflit, ni le Store schema, ni la stratégie d'acquisition. `serde_json` est utilisé uniquement dans le backend PostgreSQL pour classifier les composants du payload RAW v1 déjà canonique au moment d'un conflit.
|
||||
|
||||
Le prochain live Yellowstone + HTTP Block Polling doit permettre de distinguer immédiatement un désaccord de `slot`/`block_time`, de transaction wire, de transaction index, de version ou d'un sous-ensemble précis de `meta` (par exemple `logMessages` ou `rewards`).
|
||||
### `pre.013` — découplage borné Yellowstone Block / HTTP `getBlock`
|
||||
|
||||
Le gate de `pre.012-fix.004` est propre sur les nouveaux canaris Store et Worker. Le live Mainnet ne reproduit aucun `content_conflict`, mais reproduit deux fois un terminal Yellowstone `source_failed / onchain_transport.grpc_backpressure_overflow`. Les WARN Transport apparaissent à `22:06:52.734796` puis `22:08:39.746262`, alors que les Stop Yellowstone n'arrivent qu'à `22:07:10.781900` puis `22:08:51.337483`. Le Stop ne crée donc pas l'overflow ; il révèle ensuite le terminal déjà produit par Transport.
|
||||
|
||||
La cause est le chemin Yellowstone Block historique entièrement séquentiel :
|
||||
|
||||
```text
|
||||
Block update gRPC
|
||||
-> await HTTP getBlock
|
||||
-> await admission de toutes les transactions du bloc
|
||||
-> seulement ensuite next_update gRPC
|
||||
```
|
||||
|
||||
Cette source était en outre exclue de `uses_hydration()`, donc ne recevait aucun quota `pending/in-flight` du supervisor multi-source. Une queue Transport plus grande ne ferait que retarder le même défaut.
|
||||
|
||||
`pre.013` corrige le débit sans modifier Transport : Yellowstone Block est désormais une source hydratante pour le partitionnement des budgets existants. Un coordinateur privé de slots borne les pending ; jusqu'au quota `in-flight`, chaque tâche possédée exécute `getBlock` puis l'admission complète du bloc. La boucle source reste disponible pour `session.next_update()` tant qu'une place pending existe. Le slot n'est `settled` qu'après admission complète. Stop/abort joint toutes les tâches et purge les pending sans faux settlement.
|
||||
|
||||
La politique reste fail-closed : aucune hausse de `YellowstoneGrpcSessionSettings.update_channel_capacity`, aucune queue non bornée, aucun silent drop et aucun masquage de `grpc_backpressure_overflow`. Si le débit aval reste durablement inférieur à la chaîne malgré la concurrence bornée, une saturation terminale reste correcte et observable.
|
||||
|
||||
Canaris ajoutés :
|
||||
|
||||
```text
|
||||
v0_3_15_pre_013_yellowstone_block_hydration_coordinator_is_bounded_before_http_work
|
||||
v0_3_15_pre_013_yellowstone_block_hydration_is_bounded_concurrent_and_stop_preemptible
|
||||
v0_3_15_pre_013_yellowstone_block_backpressure_fix_adds_no_public_surface
|
||||
```
|
||||
|
||||
Aucun type du coordinateur n'est exporté publiquement.
|
||||
|
||||
Validation live requise : Yellowstone Mainnet seul pendant une durée supérieure aux ~80 s de reproduction, puis Yellowstone + HTTP Block Polling en parallèle. Le run doit rester `Healthy`, sans `grpc_backpressure_overflow`, et les Stop ciblés doivent finir `Stopped`. Le diagnostic `pre.012-fix.004` reste actif si un `content_conflict` réapparaît.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user