feat(project): resume recoverable tasks on project open
This commit is contained in:
parent
02c2ecb22c
commit
36d083f727
20 changed files with 542 additions and 66 deletions
|
|
@ -65,8 +65,8 @@ disponible. Ne jamais annoncer une vérification non exécutée.
|
||||||
- Gouverneur thread-safe avec estimations, budgets et réservations opaques.
|
- Gouverneur thread-safe avec estimations, budgets et réservations opaques.
|
||||||
- Scheduler relié au gouverneur ; réservation obligatoire avant callback.
|
- Scheduler relié au gouverneur ; réservation obligatoire avant callback.
|
||||||
- Aucun DAG, aucune priorité, aucun pool de workers.
|
- Aucun DAG, aucune priorité, aucun pool de workers.
|
||||||
- L'import asynchrone utilise encore son worker dédié et n'est pas migré vers
|
- L'import asynchrone `import.images` utilise le scheduler générique, ses lots
|
||||||
le scheduler générique.
|
adaptatifs et ses checkpoints persistants.
|
||||||
- Le viewer Vulkan n'est pas commencé.
|
- Le viewer Vulkan n'est pas commencé.
|
||||||
|
|
||||||
## Prochains tickets recommandés
|
## Prochains tickets recommandés
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
||||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||||
- **Project Database v3** : identité, tâches typées/checkpoints, paramètres d'import et inventaire d'artefacts SQLite
|
- **Project Database v3** : identité, tâches typées/checkpoints, paramètres d'import et inventaire d'artefacts SQLite
|
||||||
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
||||||
|
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
|
||||||
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
|
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
|
||||||
- **Hardware Profile** : détection des capacités matérielles
|
- **Hardware Profile** : détection des capacités matérielles
|
||||||
- **Resource Snapshot** : capture instantanée des ressources
|
- **Resource Snapshot** : capture instantanée des ressources
|
||||||
|
|
@ -40,7 +41,7 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
||||||
|
|
||||||
### Briques prévues (PLANNED)
|
### Briques prévues (PLANNED)
|
||||||
|
|
||||||
- Resoumission contrôlée des tâches récupérables à l'ouverture
|
- Graphe de dépendances pour ordonner les futures reprises interdépendantes
|
||||||
- DAG de dépendances
|
- DAG de dépendances
|
||||||
- Pools de workers multiples (CPU/GPU/IO)
|
- Pools de workers multiples (CPU/GPU/IO)
|
||||||
- Publication live validée
|
- Publication live validée
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,17 @@ checkpoint de fin de lot. Si ce checkpoint ou sa transaction DB échoue, le
|
||||||
manifeste demeure la frontière idempotente plus récente et un checkpoint
|
manifeste demeure la frontière idempotente plus récente et un checkpoint
|
||||||
orphelin peut subsister selon le protocole filesystem puis SQLite.
|
orphelin peut subsister selon le protocole filesystem puis SQLite.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave complet, resoumission scheduler et réconciliation
|
**IMPLEMENTED** — reprise automatique sélective à l'ouverture : pagination
|
||||||
des fichiers orphelins.
|
bornée, validation checkpoint/kind, reconstruction production et enqueue sans
|
||||||
|
claim persistant supplémentaire.
|
||||||
|
|
||||||
|
Les records sont parcourus par task ID croissant. Un checkpoint
|
||||||
|
`PUBLISHED_NOT_DURABLE` présent, valide et cohérent peut être repris ; le résumé
|
||||||
|
conserve cet avertissement jusqu'au prochain checkpoint durable. Une tâche
|
||||||
|
terminale n'appartient pas à la requête de reprise.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
|
||||||
|
retry piloté par l'utilisateur pour les sources indisponibles.
|
||||||
|
|
||||||
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v4+ et
|
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v4+ et
|
||||||
reprise globale.
|
reprise avec dépendances.
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,18 @@ distingue `LEGACY_UNTYPED`, `UNKNOWN_TASK_KIND` et
|
||||||
`UNSUPPORTED_TASK_KIND_VERSION`. Aucun reconstructeur métier n'est appelé sous
|
`UNSUPPORTED_TASK_KIND_VERSION`. Aucun reconstructeur métier n'est appelé sous
|
||||||
le mutex DB. Un upsert ne peut pas changer le couple kind/version d'un task ID.
|
le mutex DB. Un upsert ne peut pas changer le couple kind/version d'un task ID.
|
||||||
|
|
||||||
|
À l'ouverture, le projet lit des pages de 8 dans l'ordre croissant des task
|
||||||
|
IDs. Chaque record est copié hors mutex DB avant reconstruction et enqueue. Une
|
||||||
|
fenêtre pleine interrompt le scan sans modifier les tâches restantes. Le résumé
|
||||||
|
borné expose `inspected`, `resumed`, `skipped`, `failed`, le nombre de
|
||||||
|
checkpoints `PUBLISHED_NOT_DURABLE` repris et la saturation éventuelle.
|
||||||
|
|
||||||
|
Les erreurs d'ouverture/migration, de schéma ou d'identité restent fatales.
|
||||||
|
Les erreurs propres à une tâche — legacy, kind inconnu/futur, checkpoint
|
||||||
|
absent/invalide/futur, source indisponible ou reconstruction — sont non fatales.
|
||||||
|
Un `BUSY` après le timeout SQLite arrête le scan sans boucle et laisse le projet
|
||||||
|
ouvert.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**IMPLEMENTED** — SQLite système, schéma v3 et migrations v1→v2→v3, identité
|
**IMPLEMENTED** — SQLite système, schéma v3 et migrations v1→v2→v3, identité
|
||||||
|
|
@ -289,8 +301,11 @@ reconstruction explicite testée hors scheduler.
|
||||||
**IMPLEMENTED** — allocation transactionnelle de task IDs, paramètres
|
**IMPLEMENTED** — allocation transactionnelle de task IDs, paramètres
|
||||||
immuables de `import.images` et reconstruction production explicite.
|
immuables de `import.images` et reconstruction production explicite.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — resoumission automatique, autosave
|
**IMPLEMENTED** — reprise automatique sélective, pagination de 8, ordre par ID,
|
||||||
à toutes les transitions et réconciliation des checkpoints orphelins, ScanSet
|
fenêtre de queue non bloquante et résumé consultable.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
|
||||||
|
indisponibles et réconciliation des checkpoints orphelins, ScanSet
|
||||||
et catalogue image persistants, Feature Store et Visual Index.
|
et catalogue image persistants, Feature Store et Visual Index.
|
||||||
|
|
||||||
**PLANNED** — migrations v4+, dépendances d'artefacts, graphe géométrique et
|
**PLANNED** — migrations v4+, dépendances d'artefacts, graphe géométrique et
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
|
|
||||||
- Worker unique (pas de pools multiples)
|
- Worker unique (pas de pools multiples)
|
||||||
- Pas de parallélisme inter-tâches
|
- Pas de parallélisme inter-tâches
|
||||||
- Checkpoints isolés disponibles mais pas encore orchestrés au démarrage
|
- Reprise automatique limitée aux tâches indépendantes reconstructibles
|
||||||
|
|
||||||
## Reprise durable
|
## Reprise durable
|
||||||
|
|
||||||
|
|
@ -78,8 +78,17 @@ qu'à la terminaison de cette tentative.
|
||||||
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et
|
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et
|
||||||
se reconstruit explicitement avec un userdata neuf lié au projet rouvert.
|
se reconstruit explicitement avec un userdata neuf lié au projet rouvert.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — resoumission automatique à l'ouverture et autosave
|
**IMPLEMENTED** — `project_open()` inventorie par pages de 8, restaure puis
|
||||||
générique des autres kinds.
|
resoumet automatiquement les tâches production valides. Il retourne après
|
||||||
|
l'enqueue et n'attend jamais leur terminaison.
|
||||||
|
|
||||||
|
L'ordre d'initialisation production est : profil matériel, governor, queue et
|
||||||
|
worker, TUI, puis ouverture DB/projet et reprise synchrone. Une fermeture ne
|
||||||
|
peut commencer qu'après le retour de `project_open()`. Le worker peut consommer
|
||||||
|
pendant le scan ; chaque tâche exécutée est néanmoins réadmise normalement.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave générique des autres kinds et reprise ordonnée par
|
||||||
|
dépendances.
|
||||||
|
|
||||||
**PLANNED** — reprise globale du scheduler via la Project Database.
|
**PLANNED** — reprise globale du scheduler via la Project Database.
|
||||||
|
|
||||||
|
|
@ -99,7 +108,10 @@ mutex DB pour un kind connu ; elle ne soumet aucune tâche.
|
||||||
n'entre en collision avec aucune tâche connue. L'import production peut donc
|
n'entre en collision avec aucune tâche connue. L'import production peut donc
|
||||||
être reconstruit puis soumis explicitement.
|
être reconstruit puis soumis explicitement.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — aucune resoumission automatique à `project_open()`.
|
**IMPLEMENTED** — la resoumission automatique utilise la registry production,
|
||||||
|
conserve le task ID et laisse le worker obtenir une nouvelle réservation.
|
||||||
|
Kinds inconnus, tâches legacy, checkpoints invalides et sources absentes ne
|
||||||
|
bloquent pas l'ouverture.
|
||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,10 @@ migration DB v1→v2, classification recovery et restauration explicite testée.
|
||||||
le chemin source borné depuis la table dédiée et reconstruit callback et
|
le chemin source borné depuis la table dédiée et reconstruit callback et
|
||||||
userdata sans `AppState *` ancien.
|
userdata sans `AppState *` ancien.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave complet, resoumission automatique et
|
**IMPLEMENTED** — `project_open()` utilise la registry production immutable
|
||||||
réconciliation orpheline.
|
pour restaurer hors mutex DB et transférer chaque tâche acceptée à la queue.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave complet et réconciliation orpheline.
|
||||||
|
|
||||||
**PLANNED** — kinds des tâches ScanSet, Image Catalog, Feature Store, Visual
|
**PLANNED** — kinds des tâches ScanSet, Image Catalog, Feature Store, Visual
|
||||||
Index et reconstruction lorsque ces traitements existeront réellement.
|
Index et reconstruction lorsque ces traitements existeront réellement.
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ Le type public réel est l'opaque `Lardon3DTaskQueue`.
|
||||||
| `lardon3d_task_queue_destroy()` | Arrête, annule, attend puis libère la file |
|
| `lardon3d_task_queue_destroy()` | Arrête, annule, attend puis libère la file |
|
||||||
| `lardon3d_task_queue_add()` | Ajoute avec backpressure bloquante |
|
| `lardon3d_task_queue_add()` | Ajoute avec backpressure bloquante |
|
||||||
| `lardon3d_task_queue_try_add()` | Ajoute sans bloquer si une place existe |
|
| `lardon3d_task_queue_try_add()` | Ajoute sans bloquer si une place existe |
|
||||||
|
| `lardon3d_task_queue_try_add_ex()` | Distingue succès, saturation, arrêt, collision d'ID et erreur |
|
||||||
|
| `lardon3d_task_queue_pause()` / `resume()` | Contrôle une tâche par son ID stable |
|
||||||
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
|
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
|
||||||
| `lardon3d_task_queue_remove()` | Retire une tâche terminale |
|
| `lardon3d_task_queue_remove()` | Retire une tâche terminale |
|
||||||
| `lardon3d_task_queue_snapshot()` | Copie une vue bornée de la file |
|
| `lardon3d_task_queue_snapshot()` | Copie une vue bornée de la file |
|
||||||
|
|
@ -75,14 +77,20 @@ le blocage par la tête de file lorsqu'une tâche ne peut pas démarrer.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**IMPLÉMENTÉ** — file FIFO avec worker unique, sélection adaptative, pause et
|
**IMPLÉMENTÉ** — file FIFO avec worker unique, sélection adaptative, pause,
|
||||||
annulation coopératives.
|
annulation coopérative et accueil des tâches restaurées avec ID préassigné.
|
||||||
|
|
||||||
|
La reprise projet utilise `try_add_ex()` et ne bloque jamais `project_open()`.
|
||||||
|
À saturation, elle arrête sa fenêtre : les tâches non transférées restent
|
||||||
|
`PENDING` en DB et seront réévaluées lors d'une ouverture ultérieure. Ce n'est
|
||||||
|
pas un second scheduler.
|
||||||
|
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
- Worker unique : pas de parallélisme interne.
|
- Worker unique : pas de parallélisme interne.
|
||||||
- Pas de DAG ni de dépendances inter-tâches.
|
- Pas de DAG ni de dépendances inter-tâches.
|
||||||
- Pas de priorités (FIFO strict).
|
- Pas de priorités (FIFO strict).
|
||||||
- La reprise depuis Project Database n'est pas encore branchée.
|
- La reprise ne possède pas encore de DAG ni de déclenchement différé lorsque
|
||||||
|
une place se libère pendant la session courante.
|
||||||
- Pas de pool de workers CPU/IO/GPU.
|
- Pas de pool de workers CPU/IO/GPU.
|
||||||
- La backpressure borne les producteurs à la capacité configurée.
|
- La backpressure borne les producteurs à la capacité configurée.
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,10 @@ par la tâche restaurée et exécuté après arrêt de son exécution.
|
||||||
de séquence et les checkpoints génériques ; une tâche restaurée conserve son ID
|
de séquence et les checkpoints génériques ; une tâche restaurée conserve son ID
|
||||||
lors de sa soumission explicite à la file.
|
lors de sa soumission explicite à la file.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave générique et resoumission automatique.
|
**IMPLEMENTED** — resoumission automatique sélective des tâches production à
|
||||||
|
l'ouverture du projet.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave générique et dépendances entre tâches.
|
||||||
|
|
||||||
Le chemin de production de l'import ne possède plus de thread ni de drapeau
|
Le chemin de production de l'import ne possède plus de thread ni de drapeau
|
||||||
d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la
|
d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la
|
||||||
|
|
@ -98,6 +101,11 @@ avant l'appel hors mutex. `join()` attend la fin du callback ; le userdata reste
|
||||||
donc valide pendant celui-ci et son destructeur n'est appelé qu'ensuite par la
|
donc valide pendant celui-ci et son destructeur n'est appelé qu'ensuite par la
|
||||||
destruction de la tâche.
|
destruction de la tâche.
|
||||||
|
|
||||||
|
Une tâche reconstruite mais refusée avant transfert à la queue est abandonnée
|
||||||
|
localement : son userdata est détruit, sans callback terminal ni écriture
|
||||||
|
durable d'une fausse annulation. Une annulation explicitement demandée conserve
|
||||||
|
le contrat de notification terminale.
|
||||||
|
|
||||||
La Project Database v3 peut enregistrer transactionnellement un résumé
|
La Project Database v3 peut enregistrer transactionnellement un résumé
|
||||||
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
|
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
|
||||||
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
||||||
|
|
@ -111,6 +119,6 @@ d'inventaire retourne des snapshots durables validés, mais ne peut pas appeler
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
- Aucune priorité interne : l'ordre est uniquement FIFO.
|
- Aucune priorité interne : l'ordre est uniquement FIFO.
|
||||||
- Pas encore de reprise globale au démarrage du projet.
|
- Pas encore de DAG pour ordonner des reprises interdépendantes.
|
||||||
- Aucune dépendance inter-tâches (pas de DAG).
|
- Aucune dépendance inter-tâches (pas de DAG).
|
||||||
- Pas encore de références d'artefacts métier validés.
|
- Pas encore de références d'artefacts métier validés.
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
|
||||||
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
||||||
- ✅ Registry durable des types métier de tâches
|
- ✅ Registry durable des types métier de tâches
|
||||||
- ✅ Premier type métier reconstructible (`import.images`)
|
- ✅ Premier type métier reconstructible (`import.images`)
|
||||||
- 📋 Resoumission automatique contrôlée des tâches récupérables
|
- ✅ Resoumission automatique contrôlée et bornée des tâches récupérables
|
||||||
- 📋 ScanSet et Image Catalog persistants
|
- 📋 ScanSet et Image Catalog persistants
|
||||||
|
|
||||||
### Phase 4 : Pipeline avancé
|
### Phase 4 : Pipeline avancé
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define LARDON3D_APP_STATE_H
|
#define LARDON3D_APP_STATE_H
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <lardon3d/hardware_profile.h>
|
#include <lardon3d/hardware_profile.h>
|
||||||
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
||||||
|
|
@ -34,6 +35,12 @@ typedef struct {
|
||||||
Lardon3DHardwareProfile hardware_profile;
|
Lardon3DHardwareProfile hardware_profile;
|
||||||
Lardon3DResourceGovernor *resource_governor;
|
Lardon3DResourceGovernor *resource_governor;
|
||||||
Lardon3DProjectDb *project_db;
|
Lardon3DProjectDb *project_db;
|
||||||
|
size_t recovery_inspected;
|
||||||
|
size_t recovery_resumed;
|
||||||
|
size_t recovery_skipped;
|
||||||
|
size_t recovery_failed;
|
||||||
|
size_t recovery_published_not_durable;
|
||||||
|
bool recovery_queue_full;
|
||||||
} Lardon3DAppState;
|
} Lardon3DAppState;
|
||||||
|
|
||||||
void lardon3d_app_state_init(Lardon3DAppState *state);
|
void lardon3d_app_state_init(Lardon3DAppState *state);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,15 @@ typedef struct {
|
||||||
Lardon3DTaskDurableSnapshot snapshot;
|
Lardon3DTaskDurableSnapshot snapshot;
|
||||||
} Lardon3DProjectRecoveryEntry;
|
} Lardon3DProjectRecoveryEntry;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
size_t inspected;
|
||||||
|
size_t resumed;
|
||||||
|
size_t skipped;
|
||||||
|
size_t failed;
|
||||||
|
size_t published_not_durable;
|
||||||
|
bool queue_full;
|
||||||
|
} Lardon3DProjectRecoverySummary;
|
||||||
|
|
||||||
bool lardon3d_project_create(
|
bool lardon3d_project_create(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const char *name
|
const char *name
|
||||||
|
|
@ -68,5 +77,14 @@ Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
||||||
size_t capacity,
|
size_t capacity,
|
||||||
size_t *count
|
size_t *count
|
||||||
);
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_resume_recoverable_tasks(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
Lardon3DProjectRecoverySummary *summary
|
||||||
|
);
|
||||||
|
bool lardon3d_project_last_recovery_summary(
|
||||||
|
const Lardon3DAppState *state,
|
||||||
|
Lardon3DProjectRecoverySummary *summary
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@
|
||||||
|
|
||||||
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
|
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_TASK_QUEUE_ADD_OK = 0,
|
||||||
|
LARDON3D_TASK_QUEUE_ADD_FULL,
|
||||||
|
LARDON3D_TASK_QUEUE_ADD_STOPPING,
|
||||||
|
LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID,
|
||||||
|
LARDON3D_TASK_QUEUE_ADD_ERROR
|
||||||
|
} Lardon3DTaskQueueAddResult;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
size_t running;
|
size_t running;
|
||||||
size_t pending;
|
size_t pending;
|
||||||
|
|
@ -35,8 +43,15 @@ bool lardon3d_task_queue_try_add(
|
||||||
Lardon3DTask *task,
|
Lardon3DTask *task,
|
||||||
uint64_t *task_id
|
uint64_t *task_id
|
||||||
);
|
);
|
||||||
|
Lardon3DTaskQueueAddResult lardon3d_task_queue_try_add_ex(
|
||||||
|
Lardon3DTaskQueue *queue,
|
||||||
|
Lardon3DTask *task,
|
||||||
|
uint64_t *task_id
|
||||||
|
);
|
||||||
bool lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id);
|
bool lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id);
|
||||||
bool lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id);
|
bool lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id);
|
||||||
|
bool lardon3d_task_queue_pause(Lardon3DTaskQueue *queue, uint64_t task_id);
|
||||||
|
bool lardon3d_task_queue_resume(Lardon3DTaskQueue *queue, uint64_t task_id);
|
||||||
void lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue);
|
void lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue);
|
||||||
size_t lardon3d_task_queue_count(Lardon3DTaskQueue *queue);
|
size_t lardon3d_task_queue_count(Lardon3DTaskQueue *queue);
|
||||||
bool lardon3d_task_queue_get(
|
bool lardon3d_task_queue_get(
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,10 @@ project_test = executable(
|
||||||
'src/task.c',
|
'src/task.c',
|
||||||
'src/task_checkpoint.c',
|
'src/task_checkpoint.c',
|
||||||
'src/task_kind_registry.c',
|
'src/task_kind_registry.c',
|
||||||
|
'src/task_kinds.c',
|
||||||
|
'src/task_queue.c',
|
||||||
|
'src/import.c',
|
||||||
|
'src/import_task.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
|
|
|
||||||
146
src/project.c
146
src/project.c
|
|
@ -14,8 +14,10 @@
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/image_catalog.h>
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/image_view.h>
|
#include <lardon3d/image_view.h>
|
||||||
|
#include <lardon3d/import_task.h>
|
||||||
#include <lardon3d/project_db.h>
|
#include <lardon3d/project_db.h>
|
||||||
#include <lardon3d/task_checkpoint.h>
|
#include <lardon3d/task_checkpoint.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MAX_CREATED_DIRECTORIES = 16,
|
MAX_CREATED_DIRECTORIES = 16,
|
||||||
|
|
@ -44,6 +46,19 @@ set_status(Lardon3DAppState *state, const char *message)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
store_recovery_summary(Lardon3DAppState *state,
|
||||||
|
const Lardon3DProjectRecoverySummary *summary)
|
||||||
|
{
|
||||||
|
state->recovery_inspected = summary ? summary->inspected : 0;
|
||||||
|
state->recovery_resumed = summary ? summary->resumed : 0;
|
||||||
|
state->recovery_skipped = summary ? summary->skipped : 0;
|
||||||
|
state->recovery_failed = summary ? summary->failed : 0;
|
||||||
|
state->recovery_published_not_durable = summary
|
||||||
|
? summary->published_not_durable : 0;
|
||||||
|
state->recovery_queue_full = summary && summary->queue_full;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
set_database_status(
|
set_database_status(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
|
@ -475,6 +490,7 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
||||||
|
|
||||||
clear_catalog(state);
|
clear_catalog(state);
|
||||||
state->project_loaded = true;
|
state->project_loaded = true;
|
||||||
|
store_recovery_summary(state, NULL);
|
||||||
state->project_db = database;
|
state->project_db = database;
|
||||||
(void)copy_path(
|
(void)copy_path(
|
||||||
state->project_name,
|
state->project_name,
|
||||||
|
|
@ -798,12 +814,23 @@ lardon3d_project_open(
|
||||||
sizeof(state->project_path),
|
sizeof(state->project_path),
|
||||||
project_path
|
project_path
|
||||||
);
|
);
|
||||||
(void)snprintf(
|
store_recovery_summary(state, NULL);
|
||||||
state->status_message,
|
if (state->task_queue && state->resource_governor) {
|
||||||
sizeof(state->status_message),
|
Lardon3DProjectRecoverySummary summary;
|
||||||
"Projet ouvert : %s",
|
(void)lardon3d_project_resume_recoverable_tasks(state,
|
||||||
state->project_name
|
lardon3d_task_kind_registry_production(), &summary);
|
||||||
);
|
(void)snprintf(state->status_message, sizeof(state->status_message),
|
||||||
|
"Projet ouvert — %zu tâche(s) reprise(s), %zu ignorée(s), %zu en échec%s.",
|
||||||
|
summary.resumed, summary.skipped, summary.failed,
|
||||||
|
summary.queue_full ? ", fenêtre de reprise saturée" : "");
|
||||||
|
} else {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Projet ouvert : %s",
|
||||||
|
state->project_name
|
||||||
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -826,6 +853,7 @@ lardon3d_project_close(Lardon3DAppState *state)
|
||||||
state->project_name[0] = '\0';
|
state->project_name[0] = '\0';
|
||||||
state->project_path[0] = '\0';
|
state->project_path[0] = '\0';
|
||||||
state->project_stable_id[0] = '\0';
|
state->project_stable_id[0] = '\0';
|
||||||
|
store_recovery_summary(state, NULL);
|
||||||
set_status(state, "Projet fermé.");
|
set_status(state, "Projet fermé.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1051,3 +1079,109 @@ lardon3d_project_list_recoverable(
|
||||||
}
|
}
|
||||||
return LARDON3D_PROJECT_DB_OK;
|
return LARDON3D_PROJECT_DB_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_resume_recoverable_tasks(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
Lardon3DProjectRecoverySummary *summary
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (summary) memset(summary, 0, sizeof(*summary));
|
||||||
|
if (!state || !state->project_loaded || !state->project_db
|
||||||
|
|| !state->task_queue || !state->resource_governor || !registry
|
||||||
|
|| !summary) {
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
enum { RECOVERY_PAGE_SIZE = 8 };
|
||||||
|
uint64_t cursor = 0;
|
||||||
|
Lardon3DProjectDbResult result = LARDON3D_PROJECT_DB_OK;
|
||||||
|
bool stop = false;
|
||||||
|
do {
|
||||||
|
Lardon3DProjectRecoveryEntry entries[RECOVERY_PAGE_SIZE];
|
||||||
|
size_t count = 0;
|
||||||
|
result = lardon3d_project_list_recoverable(state, registry, cursor,
|
||||||
|
entries, RECOVERY_PAGE_SIZE, &count);
|
||||||
|
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||||
|
++summary->failed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < count; ++index) {
|
||||||
|
Lardon3DProjectRecoveryEntry *entry = &entries[index];
|
||||||
|
cursor = entry->task_id;
|
||||||
|
++summary->inspected;
|
||||||
|
bool recoverable = entry->status == LARDON3D_PROJECT_RECOVERABLE
|
||||||
|
|| entry->status
|
||||||
|
== LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE;
|
||||||
|
if (!recoverable || entry->snapshot.recovery_state != TASK_PENDING) {
|
||||||
|
++summary->skipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry->status
|
||||||
|
== LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE) {
|
||||||
|
++summary->published_not_durable;
|
||||||
|
}
|
||||||
|
Lardon3DTaskSnapshot existing;
|
||||||
|
if (lardon3d_task_queue_get(state->task_queue, entry->task_id,
|
||||||
|
&existing)) {
|
||||||
|
++summary->skipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Lardon3DImageImportReconstructionContext context = {
|
||||||
|
.project_path = state->project_path,
|
||||||
|
.project_db = state->project_db,
|
||||||
|
.resource_governor = state->resource_governor,
|
||||||
|
};
|
||||||
|
Lardon3DTask *task = NULL;
|
||||||
|
Lardon3DTaskKindResult restored =
|
||||||
|
lardon3d_task_kind_registry_restore(registry,
|
||||||
|
entry->task_kind, entry->task_kind_version,
|
||||||
|
&entry->snapshot, &context, &task);
|
||||||
|
if (restored != LARDON3D_TASK_KIND_OK || !task) {
|
||||||
|
++summary->failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Lardon3DTaskQueueAddResult added =
|
||||||
|
lardon3d_task_queue_try_add_ex(state->task_queue, task, NULL);
|
||||||
|
if (added == LARDON3D_TASK_QUEUE_ADD_OK) {
|
||||||
|
++summary->resumed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lardon3d_task_destroy(task);
|
||||||
|
if (added == LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID) {
|
||||||
|
++summary->skipped;
|
||||||
|
} else if (added == LARDON3D_TASK_QUEUE_ADD_FULL) {
|
||||||
|
++summary->skipped;
|
||||||
|
} else {
|
||||||
|
++summary->failed;
|
||||||
|
}
|
||||||
|
if (added == LARDON3D_TASK_QUEUE_ADD_FULL
|
||||||
|
|| added == LARDON3D_TASK_QUEUE_ADD_STOPPING) {
|
||||||
|
summary->queue_full = added == LARDON3D_TASK_QUEUE_ADD_FULL;
|
||||||
|
stop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stop || count < RECOVERY_PAGE_SIZE) break;
|
||||||
|
} while (cursor > 0);
|
||||||
|
store_recovery_summary(state, summary);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_project_last_recovery_summary(
|
||||||
|
const Lardon3DAppState *state,
|
||||||
|
Lardon3DProjectRecoverySummary *summary
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!state || !summary) return false;
|
||||||
|
*summary = (Lardon3DProjectRecoverySummary) {
|
||||||
|
.inspected = state->recovery_inspected,
|
||||||
|
.resumed = state->recovery_resumed,
|
||||||
|
.skipped = state->recovery_skipped,
|
||||||
|
.failed = state->recovery_failed,
|
||||||
|
.published_not_durable = state->recovery_published_not_durable,
|
||||||
|
.queue_full = state->recovery_queue_full,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -565,6 +565,12 @@ lardon3d_project_db_list_recoverable(Lardon3DProjectDb *database, uint64_t after
|
||||||
{
|
{
|
||||||
if (count) *count = 0;
|
if (count) *count = 0;
|
||||||
if (!database || !tasks || !count || after_task_id > INT64_MAX || capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
if (!database || !tasks || !count || after_task_id > INT64_MAX || capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
|
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY");
|
||||||
|
if (forced_busy && strcmp(forced_busy, "1") == 0) {
|
||||||
|
return LARDON3D_PROJECT_DB_BUSY;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL; char sql[896];
|
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL; char sql[896];
|
||||||
(void)snprintf(sql, sizeof(sql), "%s WHERE t.recovery_state=?1 AND c.task_id IS NOT NULL AND t.task_id>?2 ORDER BY t.task_id LIMIT ?3", task_select);
|
(void)snprintf(sql, sizeof(sql), "%s WHERE t.recovery_state=?1 AND c.task_id IS NOT NULL AND t.task_id>?2 ORDER BY t.task_id LIMIT ?3", task_select);
|
||||||
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
||||||
|
|
|
||||||
16
src/task.c
16
src/task.c
|
|
@ -189,7 +189,21 @@ lardon3d_task_destroy(Lardon3DTask *task)
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lardon3d_task_request_cancel(task);
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
if (!is_terminal(task->state)) {
|
||||||
|
if (task->executing) {
|
||||||
|
task->cancel_requested = true;
|
||||||
|
copy_text(task->message, sizeof(task->message),
|
||||||
|
"Annulation demandée.");
|
||||||
|
(void)pthread_cond_broadcast(&task->condition);
|
||||||
|
} else {
|
||||||
|
/* Une tâche locale jamais soumise peut être abandonnée sans
|
||||||
|
* publier une fausse annulation métier. */
|
||||||
|
finish_locked(task, TASK_CANCELLED, "Tâche abandonnée.");
|
||||||
|
task->finished_notified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
(void)lardon3d_task_join(task);
|
(void)lardon3d_task_join(task);
|
||||||
(void)pthread_cond_destroy(&task->condition);
|
(void)pthread_cond_destroy(&task->condition);
|
||||||
(void)pthread_mutex_destroy(&task->mutex);
|
(void)pthread_mutex_destroy(&task->mutex);
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,35 @@ lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id)
|
||||||
return node != NULL;
|
return node != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_task_queue_pause(Lardon3DTaskQueue *queue, uint64_t task_id)
|
||||||
|
{
|
||||||
|
if (!queue || task_id == 0) return false;
|
||||||
|
(void)pthread_mutex_lock(&queue->mutex);
|
||||||
|
TaskNode *node = queue->all_head;
|
||||||
|
while (node && lardon3d_task_id(node->task) != task_id) {
|
||||||
|
node = node->next_all;
|
||||||
|
}
|
||||||
|
bool paused = node && lardon3d_task_pause(node->task);
|
||||||
|
(void)pthread_mutex_unlock(&queue->mutex);
|
||||||
|
return paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_task_queue_resume(Lardon3DTaskQueue *queue, uint64_t task_id)
|
||||||
|
{
|
||||||
|
if (!queue || task_id == 0) return false;
|
||||||
|
(void)pthread_mutex_lock(&queue->mutex);
|
||||||
|
TaskNode *node = queue->all_head;
|
||||||
|
while (node && lardon3d_task_id(node->task) != task_id) {
|
||||||
|
node = node->next_all;
|
||||||
|
}
|
||||||
|
bool resumed = node && lardon3d_task_resume(node->task);
|
||||||
|
if (resumed) (void)pthread_cond_broadcast(&queue->not_empty);
|
||||||
|
(void)pthread_mutex_unlock(&queue->mutex);
|
||||||
|
return resumed;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue)
|
lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue)
|
||||||
{
|
{
|
||||||
|
|
@ -356,22 +385,50 @@ lardon3d_task_queue_try_add(
|
||||||
Lardon3DTask *task,
|
Lardon3DTask *task,
|
||||||
uint64_t *task_id
|
uint64_t *task_id
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
return lardon3d_task_queue_try_add_ex(queue, task, task_id)
|
||||||
|
== LARDON3D_TASK_QUEUE_ADD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DTaskQueueAddResult
|
||||||
|
lardon3d_task_queue_try_add_ex(
|
||||||
|
Lardon3DTaskQueue *queue,
|
||||||
|
Lardon3DTask *task,
|
||||||
|
uint64_t *task_id
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!queue || !task) {
|
if (!queue || !task) {
|
||||||
return false;
|
return LARDON3D_TASK_QUEUE_ADD_ERROR;
|
||||||
}
|
}
|
||||||
TaskNode *node = calloc(1, sizeof(*node));
|
TaskNode *node = calloc(1, sizeof(*node));
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return false;
|
return LARDON3D_TASK_QUEUE_ADD_ERROR;
|
||||||
}
|
}
|
||||||
(void)pthread_mutex_lock(&queue->mutex);
|
(void)pthread_mutex_lock(&queue->mutex);
|
||||||
bool accepted = queue->pending_count < queue->capacity
|
Lardon3DTaskQueueAddResult result = LARDON3D_TASK_QUEUE_ADD_OK;
|
||||||
&& enqueue_locked(queue, node, task, task_id);
|
if (queue->stopping) {
|
||||||
|
result = LARDON3D_TASK_QUEUE_ADD_STOPPING;
|
||||||
|
} else if (queue->pending_count >= queue->capacity) {
|
||||||
|
result = LARDON3D_TASK_QUEUE_ADD_FULL;
|
||||||
|
} else {
|
||||||
|
uint64_t id = lardon3d_task_id(task);
|
||||||
|
for (TaskNode *existing = queue->all_head; existing;
|
||||||
|
existing = existing->next_all) {
|
||||||
|
if (id != 0 && lardon3d_task_id(existing->task) == id) {
|
||||||
|
result = LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_TASK_QUEUE_ADD_OK
|
||||||
|
&& !enqueue_locked(queue, node, task, task_id)) {
|
||||||
|
result = LARDON3D_TASK_QUEUE_ADD_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
(void)pthread_mutex_unlock(&queue->mutex);
|
(void)pthread_mutex_unlock(&queue->mutex);
|
||||||
if (!accepted) {
|
if (result != LARDON3D_TASK_QUEUE_ADD_OK) {
|
||||||
free(node);
|
free(node);
|
||||||
}
|
}
|
||||||
return accepted;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include <lardon3d/image_catalog.h>
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/import_task.h>
|
#include <lardon3d/import_task.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
|
#include <lardon3d/task_checkpoint.h>
|
||||||
#include <lardon3d/task_queue.h>
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
#define CHECK(condition) do { if (!(condition)) { \
|
#define CHECK(condition) do { if (!(condition)) { \
|
||||||
|
|
@ -55,7 +56,7 @@ remove_tree(const char *path)
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
setup_runtime(Lardon3DAppState *state)
|
setup_runtime(Lardon3DAppState *state, size_t queue_capacity)
|
||||||
{
|
{
|
||||||
state->hardware_profile = (Lardon3DHardwareProfile) {
|
state->hardware_profile = (Lardon3DHardwareProfile) {
|
||||||
.logical_cpu_count = 1024,
|
.logical_cpu_count = 1024,
|
||||||
|
|
@ -71,7 +72,8 @@ setup_runtime(Lardon3DAppState *state)
|
||||||
state->resource_governor = lardon3d_resource_governor_create(
|
state->resource_governor = lardon3d_resource_governor_create(
|
||||||
&state->hardware_profile, &policy);
|
&state->hardware_profile, &policy);
|
||||||
state->task_queue = state->resource_governor
|
state->task_queue = state->resource_governor
|
||||||
? lardon3d_task_queue_create(state->resource_governor, 4) : NULL;
|
? lardon3d_task_queue_create(state->resource_governor, queue_capacity)
|
||||||
|
: NULL;
|
||||||
return state->task_queue != NULL;
|
return state->task_queue != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +103,7 @@ run_test(void)
|
||||||
}
|
}
|
||||||
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
||||||
Lardon3DAppState state; lardon3d_app_state_init(&state);
|
Lardon3DAppState state; lardon3d_app_state_init(&state);
|
||||||
CHECK(setup_runtime(&state));
|
CHECK(setup_runtime(&state, 4));
|
||||||
CHECK(lardon3d_project_create(&state, "Persistent Import"));
|
CHECK(lardon3d_project_create(&state, "Persistent Import"));
|
||||||
|
|
||||||
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
||||||
|
|
@ -113,42 +115,45 @@ run_test(void)
|
||||||
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
|
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
|
||||||
CHECK(runtime.progress > 0 && runtime.progress < 100);
|
CHECK(runtime.progress > 0 && runtime.progress < 100);
|
||||||
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
||||||
|
char checkpoint_path[PATH_MAX];
|
||||||
|
CHECK(snprintf(checkpoint_path, sizeof(checkpoint_path),
|
||||||
|
"%s/.lardon3d/checkpoints/%llu.chk", state.project_path,
|
||||||
|
(unsigned long long)task_id) > 0);
|
||||||
|
Lardon3DTaskDurableSnapshot persisted_snapshot;
|
||||||
|
CHECK(lardon3d_task_checkpoint_load(checkpoint_path, &persisted_snapshot,
|
||||||
|
NULL) == LARDON3D_TASK_CHECKPOINT_OK);
|
||||||
|
Lardon3DProjectDbCheckpoint published_not_durable = {
|
||||||
|
.format_version = LARDON3D_TASK_CHECKPOINT_VERSION,
|
||||||
|
.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE,
|
||||||
|
.updated_at = 1,
|
||||||
|
};
|
||||||
|
CHECK(snprintf(published_not_durable.path,
|
||||||
|
sizeof(published_not_durable.path),
|
||||||
|
".lardon3d/checkpoints/%llu.chk",
|
||||||
|
(unsigned long long)task_id) > 0);
|
||||||
|
CHECK(lardon3d_project_db_record_image_import_task(state.project_db,
|
||||||
|
&persisted_snapshot, LARDON3D_IMAGE_IMPORT_TASK_KIND,
|
||||||
|
LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION, &published_not_durable,
|
||||||
|
source, 1) == LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
|
||||||
|
|
||||||
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
||||||
|
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
||||||
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
|
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
|
||||||
Lardon3DProjectRecoveryEntry entry; size_t count = 0;
|
Lardon3DProjectRecoverySummary recovery;
|
||||||
const Lardon3DTaskKindRegistry *registry =
|
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
|
||||||
lardon3d_task_kind_registry_production();
|
CHECK(recovery.inspected == 1 && recovery.resumed == 1
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, registry, 0, &entry, 1,
|
&& recovery.skipped == 0 && recovery.failed == 0
|
||||||
&count) == LARDON3D_PROJECT_DB_OK && count == 1);
|
&& recovery.published_not_durable == 1);
|
||||||
CHECK(entry.task_id == task_id
|
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
|
||||||
&& strcmp(entry.task_kind, LARDON3D_IMAGE_IMPORT_TASK_KIND) == 0
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
||||||
&& entry.task_kind_version == LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION);
|
CHECK(lardon3d_task_queue_resume(state.task_queue, task_id));
|
||||||
Lardon3DProjectDbImageImport persisted_parameters;
|
Lardon3DProjectDbImageImport persisted_parameters;
|
||||||
CHECK(lardon3d_project_db_load_image_import(state.project_db, task_id,
|
CHECK(lardon3d_project_db_load_image_import(state.project_db, task_id,
|
||||||
&persisted_parameters) == LARDON3D_PROJECT_DB_OK);
|
&persisted_parameters) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(strcmp(persisted_parameters.source_path, source) == 0);
|
CHECK(strcmp(persisted_parameters.source_path, source) == 0);
|
||||||
Lardon3DImageImportReconstructionContext reconstruction = {
|
|
||||||
.project_path = state.project_path,
|
|
||||||
.project_db = state.project_db,
|
|
||||||
.resource_governor = state.resource_governor,
|
|
||||||
};
|
|
||||||
Lardon3DTask *restored = NULL;
|
|
||||||
char unavailable_source[PATH_MAX];
|
|
||||||
CHECK(join_path(unavailable_source, root, "source-unavailable"));
|
|
||||||
CHECK(rename(source, unavailable_source) == 0);
|
|
||||||
CHECK(lardon3d_task_kind_registry_restore(registry, entry.task_kind,
|
|
||||||
entry.task_kind_version, &entry.snapshot, &reconstruction, &restored)
|
|
||||||
== LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED);
|
|
||||||
CHECK(!restored && rename(unavailable_source, source) == 0);
|
|
||||||
CHECK(lardon3d_task_kind_registry_restore(registry, entry.task_kind,
|
|
||||||
entry.task_kind_version, &entry.snapshot, &reconstruction, &restored)
|
|
||||||
== LARDON3D_TASK_KIND_OK);
|
|
||||||
CHECK(restored && lardon3d_task_id(restored) == task_id);
|
|
||||||
CHECK(lardon3d_task_queue_add(state.task_queue, restored, NULL));
|
|
||||||
CHECK(wait_for_state(state.task_queue, task_id, TASK_COMPLETED, &runtime));
|
CHECK(wait_for_state(state.task_queue, task_id, TASK_COMPLETED, &runtime));
|
||||||
CHECK(runtime.progress == 100);
|
CHECK(runtime.progress == 100);
|
||||||
|
|
||||||
|
|
@ -162,8 +167,8 @@ run_test(void)
|
||||||
|
|
||||||
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
|
||||||
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
|
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, registry, 0, &entry, 1,
|
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery)
|
||||||
&count) == LARDON3D_PROJECT_DB_OK && count == 0);
|
&& recovery.inspected == 0 && recovery.resumed == 0);
|
||||||
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
|
@ -172,4 +177,117 @@ run_test(void)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; }
|
static bool
|
||||||
|
test_selective_capacity_one(void)
|
||||||
|
{
|
||||||
|
char root[] = "/tmp/lardon3d-import-recovery-window-XXXXXX";
|
||||||
|
CHECK(mkdtemp(root));
|
||||||
|
char missing_source[PATH_MAX], valid_source[PATH_MAX], second_valid_source[PATH_MAX];
|
||||||
|
char unavailable_source[PATH_MAX];
|
||||||
|
CHECK(join_path(missing_source, root, "missing-source"));
|
||||||
|
CHECK(join_path(valid_source, root, "valid-source"));
|
||||||
|
CHECK(join_path(second_valid_source, root, "second-valid-source"));
|
||||||
|
CHECK(join_path(unavailable_source, root, "source-unavailable"));
|
||||||
|
CHECK(mkdir(missing_source, 0700) == 0
|
||||||
|
&& mkdir(valid_source, 0700) == 0
|
||||||
|
&& mkdir(second_valid_source, 0700) == 0);
|
||||||
|
for (unsigned int index = 0; index < 40; ++index) {
|
||||||
|
char name[32], path[PATH_MAX];
|
||||||
|
CHECK(snprintf(name, sizeof(name), "missing-%03u.jpg", index) > 0);
|
||||||
|
CHECK(join_path(path, missing_source, name)
|
||||||
|
&& write_fixture(path, index));
|
||||||
|
CHECK(snprintf(name, sizeof(name), "valid-%03u.jpg", index) > 0);
|
||||||
|
CHECK(join_path(path, valid_source, name)
|
||||||
|
&& write_fixture(path, index + 100));
|
||||||
|
CHECK(snprintf(name, sizeof(name), "second-%03u.jpg", index) > 0);
|
||||||
|
CHECK(join_path(path, second_valid_source, name)
|
||||||
|
&& write_fixture(path, index + 200));
|
||||||
|
}
|
||||||
|
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
||||||
|
Lardon3DAppState state;
|
||||||
|
lardon3d_app_state_init(&state);
|
||||||
|
CHECK(setup_runtime(&state, 4));
|
||||||
|
CHECK(lardon3d_project_create(&state, "Selective Recovery"));
|
||||||
|
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
||||||
|
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1)
|
||||||
|
== 0);
|
||||||
|
uint64_t missing_id = 0, valid_id = 0, second_valid_id = 0;
|
||||||
|
CHECK(lardon3d_project_enqueue_image_import(&state, missing_source,
|
||||||
|
&missing_id));
|
||||||
|
CHECK(lardon3d_project_enqueue_image_import(&state, valid_source,
|
||||||
|
&valid_id));
|
||||||
|
CHECK(lardon3d_project_enqueue_image_import(&state, second_valid_source,
|
||||||
|
&second_valid_id));
|
||||||
|
Lardon3DTaskSnapshot snapshot;
|
||||||
|
CHECK(wait_for_state(state.task_queue, missing_id, TASK_PAUSED, &snapshot));
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_project_close(&state);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
|
||||||
|
CHECK(rename(missing_source, unavailable_source) == 0);
|
||||||
|
|
||||||
|
const Lardon3DResourceEstimate held_estimate = {
|
||||||
|
.minimum_batch_size = 1,
|
||||||
|
.maximum_batch_size = 1,
|
||||||
|
.desired_cpu_threads = 1,
|
||||||
|
.desired_io_slots = 1,
|
||||||
|
.task_class = LARDON3D_RESOURCE_TASK_IMPORT,
|
||||||
|
};
|
||||||
|
Lardon3DResourceSnapshot available = {
|
||||||
|
.memory_available_bytes = UINT64_MAX,
|
||||||
|
.cpu_load_1m = 0.0,
|
||||||
|
};
|
||||||
|
Lardon3DResourceDecision decision;
|
||||||
|
Lardon3DResourceReservation *held = NULL;
|
||||||
|
CHECK(lardon3d_resource_governor_reserve(state.resource_governor,
|
||||||
|
&available, &held_estimate, &decision, &held));
|
||||||
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
|
||||||
|
CHECK(state.task_queue
|
||||||
|
&& lardon3d_project_open(&state, "Selective Recovery"));
|
||||||
|
Lardon3DProjectRecoverySummary recovery;
|
||||||
|
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
|
||||||
|
CHECK(recovery.inspected == 3 && recovery.resumed == 1
|
||||||
|
&& recovery.skipped == 1 && recovery.failed == 1
|
||||||
|
&& recovery.queue_full);
|
||||||
|
CHECK(lardon3d_resource_governor_release(state.resource_governor, held));
|
||||||
|
lardon3d_task_queue_resources_changed(state.task_queue);
|
||||||
|
CHECK(wait_for_state(state.task_queue, valid_id, TASK_COMPLETED, &snapshot));
|
||||||
|
CHECK(snapshot.id == valid_id && snapshot.progress == 100);
|
||||||
|
CHECK(!lardon3d_task_queue_get(state.task_queue, missing_id, &snapshot));
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_project_close(&state);
|
||||||
|
|
||||||
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
|
||||||
|
CHECK(state.task_queue
|
||||||
|
&& lardon3d_project_open(&state, "Selective Recovery"));
|
||||||
|
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
|
||||||
|
CHECK(recovery.inspected == 2 && recovery.resumed == 1
|
||||||
|
&& recovery.failed == 1);
|
||||||
|
CHECK(wait_for_state(state.task_queue, second_valid_id, TASK_COMPLETED,
|
||||||
|
&snapshot));
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_project_close(&state);
|
||||||
|
|
||||||
|
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
|
||||||
|
CHECK(state.task_queue
|
||||||
|
&& lardon3d_project_open(&state, "Selective Recovery"));
|
||||||
|
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
|
||||||
|
CHECK(recovery.inspected == 1 && recovery.resumed == 0
|
||||||
|
&& recovery.failed == 1);
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_project_close(&state);
|
||||||
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
CHECK(unsetenv("LARDON3D_PROJECTS_ROOT") == 0);
|
||||||
|
CHECK(remove_tree(root));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
return run_test() && test_selective_capacity_one()
|
||||||
|
? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/task_checkpoint.h>
|
#include <lardon3d/task_checkpoint.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
#define CHECK(condition) do { if (!(condition)) { \
|
#define CHECK(condition) do { if (!(condition)) { \
|
||||||
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
|
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
|
||||||
|
|
@ -220,6 +221,33 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE);
|
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE);
|
||||||
|
Lardon3DHardwareProfile recovery_profile = {
|
||||||
|
.logical_cpu_count = 2,
|
||||||
|
.page_size_bytes = 4096,
|
||||||
|
.memory_total_bytes = UINT64_MAX,
|
||||||
|
.cpu_architecture = "test",
|
||||||
|
};
|
||||||
|
Lardon3DResourcePolicy recovery_policy = {
|
||||||
|
.maximum_cpu_load_ratio = 1.0,
|
||||||
|
.maximum_io_pressure_avg10 = 100.0,
|
||||||
|
.io_slot_capacity = 1,
|
||||||
|
};
|
||||||
|
state.resource_governor = lardon3d_resource_governor_create(
|
||||||
|
&recovery_profile, &recovery_policy);
|
||||||
|
state.task_queue = state.resource_governor
|
||||||
|
? lardon3d_task_queue_create(state.resource_governor, 1) : NULL;
|
||||||
|
CHECK(state.task_queue);
|
||||||
|
Lardon3DProjectRecoverySummary busy_summary;
|
||||||
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY", "1", 1) == 0);
|
||||||
|
CHECK(lardon3d_project_resume_recoverable_tasks(&state, ®istry,
|
||||||
|
&busy_summary) == LARDON3D_PROJECT_DB_BUSY);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY") == 0);
|
||||||
|
CHECK(busy_summary.inspected == 0 && busy_summary.resumed == 0
|
||||||
|
&& busy_summary.failed == 1);
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
state.resource_governor = NULL;
|
||||||
CHECK(strcmp(entries[0].task_kind, "test.persisted") == 0
|
CHECK(strcmp(entries[0].task_kind, "test.persisted") == 0
|
||||||
&& entries[0].task_kind_version == 1);
|
&& entries[0].task_kind_version == 1);
|
||||||
Lardon3DTask *restored = NULL;
|
Lardon3DTask *restored = NULL;
|
||||||
|
|
@ -275,6 +303,25 @@ run_test(void)
|
||||||
== LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION);
|
== LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION);
|
||||||
CHECK(negative_entries[3].task_id == 6
|
CHECK(negative_entries[3].task_id == 6
|
||||||
&& negative_entries[3].status == LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED);
|
&& negative_entries[3].status == LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED);
|
||||||
|
state.resource_governor = lardon3d_resource_governor_create(
|
||||||
|
&recovery_profile, &recovery_policy);
|
||||||
|
state.task_queue = state.resource_governor
|
||||||
|
? lardon3d_task_queue_create(state.resource_governor, 4) : NULL;
|
||||||
|
CHECK(state.task_queue);
|
||||||
|
Lardon3DProjectRecoverySummary selective_summary;
|
||||||
|
CHECK(lardon3d_project_resume_recoverable_tasks(&state, ®istry,
|
||||||
|
&selective_summary) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(selective_summary.inspected == 4 && selective_summary.resumed == 1
|
||||||
|
&& selective_summary.skipped == 3 && selective_summary.failed == 0);
|
||||||
|
Lardon3DTaskSnapshot resumed_snapshot;
|
||||||
|
CHECK(lardon3d_task_queue_get(state.task_queue, 1, &resumed_snapshot));
|
||||||
|
CHECK(lardon3d_project_resume_recoverable_tasks(&state, ®istry,
|
||||||
|
&selective_summary) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(selective_summary.resumed == 0 && selective_summary.skipped == 4);
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
state.task_queue = NULL;
|
||||||
|
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||||
|
state.resource_governor = NULL;
|
||||||
lardon3d_task_destroy(legacy);
|
lardon3d_task_destroy(legacy);
|
||||||
lardon3d_task_destroy(future_kind);
|
lardon3d_task_destroy(future_kind);
|
||||||
lardon3d_task_destroy(unknown);
|
lardon3d_task_destroy(unknown);
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,8 @@ run_test(void)
|
||||||
Lardon3DTask *duplicate = lardon3d_task_create(
|
Lardon3DTask *duplicate = lardon3d_task_create(
|
||||||
"Doublon", &estimate, queue_callback, &restored_work);
|
"Doublon", &estimate, queue_callback, &restored_work);
|
||||||
CHECK(duplicate && lardon3d_task_assign_id(duplicate, 100));
|
CHECK(duplicate && lardon3d_task_assign_id(duplicate, 100));
|
||||||
CHECK(!lardon3d_task_queue_add(queue, duplicate, NULL));
|
CHECK(lardon3d_task_queue_try_add_ex(queue, duplicate, NULL)
|
||||||
|
== LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID);
|
||||||
lardon3d_task_destroy(duplicate);
|
lardon3d_task_destroy(duplicate);
|
||||||
CHECK(wait_terminal(queue, 100, &snapshot));
|
CHECK(wait_terminal(queue, 100, &snapshot));
|
||||||
CHECK(lardon3d_task_queue_remove(queue, 100));
|
CHECK(lardon3d_task_queue_remove(queue, 100));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue