v0.3.6-pre.012

This commit is contained in:
2026-09-01 17:42:24 +02:00
parent 9506df9487
commit 42374115c7
18 changed files with 869 additions and 88 deletions

View File

@@ -0,0 +1,72 @@
<!-- file: crates/ksp-job-api/README.md -->
<!-- version: 1 -->
# ksp-job-api
`ksp-job-api` fournit les contrats passifs et runtime-neutral communs aux jobs bornés de KSP.
La crate possède l'identité logique d'un job, son lifecycle terminable, l'intention d'annulation coopérative et le contrat latest-value utilisé pour observer un snapshot complet. Elle ne possède aucun runtime concret, aucune politique métier de backfill, aucun Transport, aucun Store et aucun Worker.
## Responsabilités
La façade crate-root expose :
- `JobId` et `JobKindCode`, bornés et validés ;
- `JobState` et `JobCompletion` ;
- `JobLifecycle`, propriétaire des transitions admises ;
- `JobCancellationToken`, cloneable et idempotent ;
- `JobNotificationSequence`, strictement monotone et sans wrap silencieux ;
- `JobNotification<S>`, valeur observable complète à une position donnée ;
- `JobSnapshotSource`, contrat runtime-neutral de lecture courante et attente d'une valeur plus récente ;
- les codes d'erreur Job et les types `Error`/`Result` communs de Core.
## Lifecycle
Le lifecycle admis reste explicitement borné :
```text
Created
| | +----------------------> Cancelled
v
Running
| | +----> Cancelling -----> Cancelled
| | | | +---------> Failed
| +-------------> Completed(Complete|Partial)
|
+------------------------> Completed(Complete|Partial)
+------------------------> Failed
```
Tout état terminal est immuable. Une transition invalide retourne `ERROR_CODE_JOB_TRANSITION_INVALID` sans modifier l'état source.
## Observation latest-value
`JobSnapshotSource` n'impose ni callback, ni queue d'événements, ni runtime async particulier. Un listener peut :
1. lire la valeur complète courante ;
2. mémoriser sa `JobNotificationSequence` ;
3. attendre une valeur plus récente ;
4. recevoir directement la dernière valeur complète, même si plusieurs mises à jour intermédiaires ont été coalescées.
Le snapshot concret appartient au job qui implémente la source. `ksp-job-api` ne connaît pas son contenu.
## Annulation
`JobCancellationToken` représente uniquement une intention coopérative partagée. Il ne tue pas une tâche, n'annule pas une I/O par lui-même et ne décide pas du résultat terminal. Le runtime concret reste responsable d'observer le token aux frontières sûres et de publier son état final.
## Firewall
La dépendance normale est volontairement minimale :
```text
ksp-job-api
-> ksp-core-lib
```
La crate ne dépend pas de Tokio, Futures, Transport, Store, Config, Logging, serde ni d'une crate de job concret.
## Documentation
- [`USAGE.md`](USAGE.md) — utilisation durable des contrats Job ;
- [`../../docs/architecture/003-COMPONENT_CONTRACTS.md`](../../docs/architecture/003-COMPONENT_CONTRACTS.md) — contrats de composants ;
- [`../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md`](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md) — séparation workers/jobs et premier backfill RAW.

141
crates/ksp-job-api/USAGE.md Normal file
View File

@@ -0,0 +1,141 @@
<!-- file: crates/ksp-job-api/USAGE.md -->
<!-- version: 1 -->
# Utilisation de ksp-job-api
Cette page décrit la façade publique durable de `ksp-job-api`. Les consumers utilisent uniquement les exports du crate-root.
## Construire une identité de job
```rust
fn job_identity() -> ksp_job_api::Result<(ksp_job_api::JobId, ksp_job_api::JobKindCode)> {
let id = match ksp_job_api::JobId::new("raw-backfill-mainnet-0001") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kind = match ksp_job_api::JobKindCode::new("raw_transaction_backfill") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok((id, kind));
}
```
`JobId` identifie un job logique et ses reprises contrôlées. `JobKindCode` identifie une famille de jobs. Les deux sont bornés et utilisent un alphabet sûr ; leur `Debug` n'est pas une surface destinée à transporter un payload métier.
## Piloter un lifecycle passif
```rust
fn lifecycle(id: ksp_job_api::JobId, kind: ksp_job_api::JobKindCode) -> ksp_job_api::Result<ksp_job_api::JobLifecycle> {
let mut lifecycle = ksp_job_api::JobLifecycle::new(id, kind);
if let std::result::Result::Err(error) = lifecycle.start() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = lifecycle.complete(ksp_job_api::JobCompletion::Complete) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(lifecycle);
}
```
Le producer ne doit pas forcer un état directement. Il utilise les opérations de `JobLifecycle`, qui refusent les transitions hors matrice.
Pour une annulation observée pendant l'exécution :
```rust
if let std::result::Result::Err(error) = lifecycle.mark_cancelling() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = lifecycle.mark_cancelled() {
return std::result::Result::Err(error);
}
```
## Partager une intention d'annulation
```rust
let token = ksp_job_api::JobCancellationToken::new();
let listener = token.clone();
assert!(!listener.is_cancellation_requested());
assert!(token.request_cancellation());
assert!(listener.is_cancellation_requested());
assert!(!token.request_cancellation());
```
Le premier appel qui change l'état retourne `true`. Les demandes suivantes sont idempotentes et retournent `false`.
Le token ne doit pas être interprété comme une primitive de kill : le runtime concret décide où l'annulation peut interrompre l'admission ou une attente et quelles opérations déjà engagées doivent être drainées.
## Publier une valeur latest-value
Un producer concret peut construire une notification complète :
```rust
#[derive(Clone)]
struct Snapshot {
completed: usize,
}
let id = ksp_job_api::JobId::new("job-0001")?;
let kind = ksp_job_api::JobKindCode::new("example")?;
let sequence = ksp_job_api::JobNotificationSequence::initial();
let notification = ksp_job_api::JobNotification::new(
id,
kind,
sequence,
ksp_job_api::JobState::Running,
Snapshot { completed: 0 },
);
assert_eq!(notification.sequence().value(), 0);
assert_eq!(notification.state(), ksp_job_api::JobState::Running);
```
Le `Debug` de `JobNotification<S>` masque volontairement le snapshot. Le type concret `S` doit lui-même rester sûr à exposer lorsque le consumer accède explicitement à `snapshot()`.
## Implémenter une source de snapshots
Une implémentation concrète possède son runtime et expose seulement le contrat `JobSnapshotSource` :
```rust
async fn observe<S>(source: &S)
where
S: ksp_job_api::JobSnapshotSource,
{
let current = source.current();
let observed = current.sequence();
let newer = source.wait_for_change(observed).await;
assert!(newer.sequence().is_after(observed));
}
```
`wait_for_change` retourne la dernière valeur complète connue après coalescence éventuelle. Un consumer ne doit donc pas supposer qu'il recevra chaque mise à jour intermédiaire.
## Faire avancer une séquence
```rust
let first = ksp_job_api::JobNotificationSequence::initial();
let second = match first.next() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(second.is_after(first));
```
L'épuisement de `u64` est une erreur explicite ; la séquence ne wrappe jamais silencieusement.
## Frontières à respecter
Ne pas ajouter à `ksp-job-api` :
```text
Tokio/Futures runtime concret
Transport ou Store
Config/Logging
DTO métier d'un job précis
scheduler, worker ou control plane
persistence de checkpoint
```
Ces responsabilités appartiennent aux crates concrètes et à la composition supérieure.