feat(task): add durable task kind registry
This commit is contained in:
parent
efa556a824
commit
2c122e495f
19 changed files with 925 additions and 86 deletions
|
|
@ -27,7 +27,8 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
||||||
- **Image View** : vues triées et filtrées pour la TUI
|
- **Image View** : vues triées et filtrées pour la TUI
|
||||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||||
- **Project Database v1** : identité, tâches/checkpoints et inventaire d'artefacts SQLite
|
- **Project Database v2** : identité, tâches typées/checkpoints et inventaire d'artefacts SQLite
|
||||||
|
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
||||||
- **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)
|
||||||
|
|
||||||
- Reconstruction des callbacks métier et resoumission des tâches récupérables
|
- Premiers types métier reconstructibles et resoumission contrôlée des tâches récupérables
|
||||||
- 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
|
||||||
|
|
@ -94,6 +95,7 @@ Acquisitions
|
||||||
- [Vue d'ensemble](docs/architecture/overview.md)
|
- [Vue d'ensemble](docs/architecture/overview.md)
|
||||||
- [Runtime](docs/architecture/runtime.md)
|
- [Runtime](docs/architecture/runtime.md)
|
||||||
- [Système de tâches](docs/architecture/task_system.md)
|
- [Système de tâches](docs/architecture/task_system.md)
|
||||||
|
- [Registry des types de tâches](docs/architecture/task_kind_registry.md)
|
||||||
- [File de tâches](docs/architecture/task_queue.md)
|
- [File de tâches](docs/architecture/task_queue.md)
|
||||||
- [Resource Governor](docs/architecture/resource_governor.md)
|
- [Resource Governor](docs/architecture/resource_governor.md)
|
||||||
- [Intégration Scheduler ↔ Governor](docs/architecture/scheduler_resource_integration.md)
|
- [Intégration Scheduler ↔ Governor](docs/architecture/scheduler_resource_integration.md)
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ par le `time_t` local avant conversion. Le format reste donc lisible entre
|
||||||
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
||||||
`time_t`.
|
`time_t`.
|
||||||
|
|
||||||
## Project Database v1
|
## Project Database v2
|
||||||
|
|
||||||
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
||||||
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
||||||
|
|
@ -135,19 +135,29 @@ L'inventaire distingue checkpoint récupérable durable, récupérable mais publ
|
||||||
non durable, absent, invalide, version inconnue et erreur d'I/O. Aucune réparation
|
non durable, absent, invalide, version inconnue et erreur d'I/O. Aucune réparation
|
||||||
ou suppression silencieuse n'est effectuée.
|
ou suppression silencieuse n'est effectuée.
|
||||||
|
|
||||||
|
Le format checkpoint reste en version 1 et ne contient pas de `task_kind`. Le
|
||||||
|
schéma SQLite v2 conserve `task_kind` et `task_kind_version` dans le résumé
|
||||||
|
logique interrogable. La migration v1→v2 laisse ces deux colonnes à `NULL` : une
|
||||||
|
tâche legacy reste inspectable mais ne peut pas être reconstruite ou resoumise.
|
||||||
|
Un kind inconnu ou une version non supportée est diagnostiqué sans exécuter de
|
||||||
|
code.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication
|
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication
|
||||||
atomique et restauration sûre d'une tâche isolée.
|
atomique et restauration sûre d'une tâche isolée.
|
||||||
|
|
||||||
**IMPLEMENTED** — Project Database v1 pour identité, résumés de tâches,
|
**IMPLEMENTED** — Project Database v2 pour identité, résumés de tâches typées,
|
||||||
références checkpoint et artefacts génériques.
|
références checkpoint et artefacts génériques.
|
||||||
|
|
||||||
|
**IMPLEMENTED** — registry statique bornée et reconstruction explicite avec
|
||||||
|
ownership du userdata.
|
||||||
|
|
||||||
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au
|
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au
|
||||||
redémarrage.
|
redémarrage.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave complet, reconstruction des callbacks métier,
|
**NOT_YET_WIRED** — autosave complet, premier type métier de production,
|
||||||
resoumission scheduler et réconciliation des fichiers orphelins.
|
resoumission scheduler et réconciliation des fichiers orphelins.
|
||||||
|
|
||||||
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v2+ et
|
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v3+ et
|
||||||
reprise globale.
|
reprise globale.
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ La base de données projet stocke les métadonnées de reconstruction et les rel
|
||||||
## Frontière avec les checkpoints de tâche
|
## Frontière avec les checkpoints de tâche
|
||||||
|
|
||||||
Le modèle durable v1 et son codec fichier sont implémentés indépendamment du
|
Le modèle durable v1 et son codec fichier sont implémentés indépendamment du
|
||||||
stockage. La future base réutilisera les mêmes règles de normalisation et de
|
stockage. La base réutilise les mêmes règles de normalisation et de
|
||||||
validation ; elle ne stockera jamais les objets pthread, callbacks, pointeurs,
|
validation ; elle ne stockera jamais les objets pthread, callbacks, pointeurs,
|
||||||
contrats ou réservations. Le fichier par tâche est une fondation, pas une
|
contrats ou réservations. Le fichier par tâche est une fondation, pas une
|
||||||
Project Database miniature.
|
Project Database miniature.
|
||||||
|
|
@ -167,12 +167,12 @@ référence vers le fichier checkpoint. Le fichier checkpoint validé reste la
|
||||||
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
|
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
|
||||||
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
||||||
|
|
||||||
## Schéma v1 implémenté
|
## Schéma v2 implémenté
|
||||||
|
|
||||||
- `metadata(key PRIMARY KEY, value)` contient uniquement `schema_version=1`.
|
- `metadata(key PRIMARY KEY, value)` contient uniquement `schema_version=2`.
|
||||||
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
|
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
|
||||||
l'unique identité logique de la DB.
|
l'unique identité logique de la DB.
|
||||||
- `tasks(task_id PRIMARY KEY, name, saved_state, recovery_state, progress,
|
- `tasks(task_id PRIMARY KEY, name, task_kind, task_kind_version, saved_state, recovery_state, progress,
|
||||||
sequence_count, started_sec/nsec, finished_sec/nsec, updated_at)` contient le
|
sequence_count, started_sec/nsec, finished_sec/nsec, updated_at)` contient le
|
||||||
résumé durable. Les IDs v1 sont compris entre 1 et `INT64_MAX`.
|
résumé durable. Les IDs v1 sont compris entre 1 et `INT64_MAX`.
|
||||||
- `checkpoints(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE, path,
|
- `checkpoints(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE, path,
|
||||||
|
|
@ -187,13 +187,26 @@ Les indexes portent uniquement sur `tasks(recovery_state, task_id)`,
|
||||||
|
|
||||||
## Ouverture et migrations
|
## Ouverture et migrations
|
||||||
|
|
||||||
Une DB vide reçoit le schéma v1 dans une transaction `BEGIN IMMEDIATE`. Une DB
|
Une DB vide reçoit directement le schéma v2 dans une transaction
|
||||||
v1 est validée puis ouverte. Une version future est refusée et une DB contenant
|
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
|
||||||
|
`task_kind` et `task_kind_version`, puis passe à v2. Les anciennes lignes restent
|
||||||
|
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
|
||||||
|
artefacts. Une interruption ou erreur provoque un rollback complet. Une DB v2
|
||||||
|
est validée puis ouverte. Une version future est refusée et une DB contenant
|
||||||
des tables sans métadonnée de version est considérée corrompue. La fonction
|
des tables sans métadonnée de version est considérée corrompue. La fonction
|
||||||
interne de migration ne connaît que `0 → 1`; aucune migration fictive ou
|
interne de migration ne connaît que `0 → 2` et `1 → 2`.
|
||||||
destructive n'est présente.
|
|
||||||
|
|
||||||
Configuration v1 : `foreign_keys=ON`, `journal_mode=DELETE`,
|
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE tasks ADD COLUMN task_kind TEXT;
|
||||||
|
ALTER TABLE tasks ADD COLUMN task_kind_version INTEGER
|
||||||
|
CHECK(task_kind_version IS NULL OR task_kind_version > 0);
|
||||||
|
UPDATE metadata SET value=2
|
||||||
|
WHERE key='schema_version' AND value=1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration v2 : `foreign_keys=ON`, `journal_mode=DELETE`,
|
||||||
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
|
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
|
||||||
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
|
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
|
||||||
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
|
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
|
||||||
|
|
@ -235,17 +248,25 @@ Les checkpoints sont référencés par chemins relatifs portables :
|
||||||
résout ce chemin sous la racine, charge le checkpoint et vérifie la cohérence du
|
résout ce chemin sous la racine, charge le checkpoint et vérifie la cohérence du
|
||||||
snapshot avec le résumé DB.
|
snapshot avec le résumé DB.
|
||||||
|
|
||||||
|
Après copie du record hors mutex SQLite, l'inventaire consulte la registry. Il
|
||||||
|
distingue `LEGACY_UNTYPED`, `UNKNOWN_TASK_KIND` et
|
||||||
|
`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.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**IMPLEMENTED** — SQLite système, schéma/migration v1, identité projet,
|
**IMPLEMENTED** — SQLite système, schéma v2 et migration v1→v2, identité projet,
|
||||||
transactions tâche+checkpoint, pagination de reprise et artefacts génériques.
|
transactions tâche+checkpoint, pagination de reprise et artefacts génériques.
|
||||||
|
|
||||||
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,
|
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,
|
||||||
publication de checkpoints par le projet et inventaire de reprise validé.
|
publication de checkpoints par le projet et inventaire de reprise validé.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — reconstruction des callbacks/userdata métier, resoumission
|
**IMPLEMENTED** — kinds persistants, classification par registry et
|
||||||
scheduler, autosave à toutes les transitions et réconciliation des checkpoints
|
reconstruction explicite testée hors scheduler.
|
||||||
orphelins, ScanSet et catalogue image persistants, Feature Store et Visual Index.
|
|
||||||
|
|
||||||
**PLANNED** — migrations v2+, dépendances d'artefacts, graphe géométrique et
|
**NOT_YET_WIRED** — type métier de production, resoumission scheduler, autosave
|
||||||
|
à toutes les transitions et réconciliation des checkpoints orphelins, ScanSet
|
||||||
|
et catalogue image persistants, Feature Store et Visual Index.
|
||||||
|
|
||||||
|
**PLANNED** — migrations v3+, dépendances d'artefacts, graphe géométrique et
|
||||||
reconstruction incrémentale.
|
reconstruction incrémentale.
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,10 @@ les opérations multi-tables sont transactionnelles et bornées.
|
||||||
l'identité et ferme la connexion. L'application arrête la task queue avant la
|
l'identité et ferme la connexion. L'application arrête la task queue avant la
|
||||||
fermeture finale du projet.
|
fermeture finale du projet.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — aucune reconstruction de callback/userdata ni resoumission
|
**IMPLEMENTED** — la registry reconstruit explicitement callback/userdata hors
|
||||||
|
mutex DB pour un kind connu ; elle ne soumet aucune tâche.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — aucun type métier de production ni resoumission
|
||||||
automatique. Une future resoumission devra aussi permettre à la queue de
|
automatique. Une future resoumission devra aussi permettre à la queue de
|
||||||
conserver l'identifiant stable restauré au lieu d'en assigner un nouveau.
|
conserver l'identifiant stable restauré au lieu d'en assigner un nouveau.
|
||||||
|
|
||||||
|
|
|
||||||
44
docs/architecture/task_kind_registry.md
Normal file
44
docs/architecture/task_kind_registry.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Registry des types métier de tâches
|
||||||
|
|
||||||
|
## Contrat
|
||||||
|
|
||||||
|
Une instance possède un **task ID** stable. Son **task kind** décrit son
|
||||||
|
comportement métier, son **task state** décrit son état d'avancement, la
|
||||||
|
**checkpoint version** décrit le codec générique et la **task kind version**
|
||||||
|
versionne les paramètres nécessaires au reconstructeur. Ces identités ne sont
|
||||||
|
pas interchangeables.
|
||||||
|
|
||||||
|
Le kind v1 est une chaîne ASCII de 1 à 64 caractères au format
|
||||||
|
`[a-z0-9][a-z0-9._-]*`. La version est un entier non nul. Aucun kind n'est
|
||||||
|
déduit d'un nom, callback ou pointeur et aucune normalisation n'est effectuée.
|
||||||
|
|
||||||
|
## Registry et ownership
|
||||||
|
|
||||||
|
La registry est une vue bornée à 64 descriptors sur un tableau statique
|
||||||
|
immutable. Le lookup est linéaire, déterministe, sans allocation et sûr en
|
||||||
|
lecture concurrente. Elle ne charge aucun code dynamiquement.
|
||||||
|
|
||||||
|
Un descriptor contient exactement le kind, sa version et un reconstructeur.
|
||||||
|
Le reconstructeur produit callback, userdata et destructeur optionnel. Avant le
|
||||||
|
transfert, la registry nettoie le userdata sur toute erreur ; après restauration
|
||||||
|
réussie, `Lardon3DTask` en devient propriétaire et le détruit une fois après la
|
||||||
|
fin de l'exécution. Le constructeur métier n'est jamais appelé sous mutex DB.
|
||||||
|
|
||||||
|
## Persistance et legacy
|
||||||
|
|
||||||
|
Le checkpoint générique reste en version 1. Project Database v2 conserve le
|
||||||
|
kind/version ; les lignes migrées depuis v1 restent `NULL/NULL` et sont classées
|
||||||
|
`LEGACY_UNTYPED`. Un kind inconnu ou une version non supportée reste inspectable
|
||||||
|
mais inexécutable. Aucun type n'est inventé et aucun code n'est sélectionné par
|
||||||
|
adresse persistée.
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
**IMPLEMENTED** — identité typée immutable, registry statique, lookup,
|
||||||
|
migration DB v1→v2, classification recovery et restauration explicite testée.
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — type métier de production, paramètres métier persistants,
|
||||||
|
autosave complet, resoumission automatique et réconciliation orpheline.
|
||||||
|
|
||||||
|
**PLANNED** — kinds des tâches ScanSet, Image Catalog, Feature Store, Visual
|
||||||
|
Index et reconstruction lorsque ces traitements existeront réellement.
|
||||||
|
|
@ -39,6 +39,8 @@ une opération de réadmission, pas un état supplémentaire.
|
||||||
|---|---|
|
|---|---|
|
||||||
| `lardon3d_task_durable_snapshot()` | Copie les champs durables sous mutex |
|
| `lardon3d_task_durable_snapshot()` | Copie les champs durables sous mutex |
|
||||||
| `lardon3d_task_restore()` | Reconstruit une tâche sans état d'exécution vivant |
|
| `lardon3d_task_restore()` | Reconstruit une tâche sans état d'exécution vivant |
|
||||||
|
| `lardon3d_task_create_typed()` | Crée une tâche persistable avec kind/version immuables |
|
||||||
|
| `lardon3d_task_restore_typed()` | Restaure une tâche typée et transfère l'ownership du userdata |
|
||||||
| `lardon3d_task_checkpoint_save()` | Publie atomiquement un snapshot v1 |
|
| `lardon3d_task_checkpoint_save()` | Publie atomiquement un snapshot v1 |
|
||||||
| `lardon3d_task_checkpoint_load()` | Lit et valide un checkpoint borné |
|
| `lardon3d_task_checkpoint_load()` | Lit et valide un checkpoint borné |
|
||||||
|
|
||||||
|
|
@ -73,9 +75,14 @@ mais sa durabilité après crash n'est pas confirmée.
|
||||||
**IMPLEMENTED** — cycle de vie, pause/annulation coopératives, séquences
|
**IMPLEMENTED** — cycle de vie, pause/annulation coopératives, séquences
|
||||||
adaptatives et fondation de checkpoints persistants isolés.
|
adaptatives et fondation de checkpoints persistants isolés.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — sauvegarde automatique et restauration par la file.
|
**IMPLEMENTED** — une registry statique peut reconstruire explicitement le
|
||||||
|
callback et le userdata d'un kind connu. Le destructeur du userdata est détenu
|
||||||
|
par la tâche restaurée et exécuté après arrêt de son exécution.
|
||||||
|
|
||||||
La Project Database v1 peut enregistrer transactionnellement un résumé
|
**NOT_YET_WIRED** — sauvegarde automatique, types métier de production et
|
||||||
|
restauration par la file.
|
||||||
|
|
||||||
|
La Project Database v2 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
|
||||||
pas la validation du fichier checkpoint avant `task_restore()`.
|
pas la validation du fichier checkpoint avant `task_restore()`.
|
||||||
|
|
@ -83,8 +90,7 @@ pas la validation du fichier checkpoint avant `task_restore()`.
|
||||||
`lardon3d_project_checkpoint_task()` est la frontière runtime : elle capture le
|
`lardon3d_project_checkpoint_task()` est la frontière runtime : elle capture le
|
||||||
snapshot, publie le fichier hors mutex de tâche, puis met à jour la DB. L'API
|
snapshot, publie le fichier hors mutex de tâche, puis met à jour la DB. L'API
|
||||||
d'inventaire retourne des snapshots durables validés, mais ne peut pas appeler
|
d'inventaire retourne des snapshots durables validés, mais ne peut pas appeler
|
||||||
`task_restore()` tant que le type métier ne sait pas reconstruire callback et
|
`task_restore()` que via un descriptor connu ; aucun pointeur n'est persistant.
|
||||||
userdata.
|
|
||||||
|
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,10 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
|
||||||
|
|
||||||
### Phase 3 : Persistance
|
### Phase 3 : Persistance
|
||||||
- ✅ Fondation versionnée des checkpoints de tâches
|
- ✅ Fondation versionnée des checkpoints de tâches
|
||||||
- ✅ Project Database v1 (SQLite)
|
- ✅ Project Database v2 (SQLite, migration v1 et tâches typées)
|
||||||
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
||||||
- 📋 Reconstruction métier et resoumission des tâches récupérables
|
- ✅ Registry durable des types métier de tâches
|
||||||
|
- 📋 Premier type métier reconstructible et resoumission des tâches récupérables
|
||||||
- 📋 ScanSet et Image Catalog persistants
|
- 📋 ScanSet et Image Catalog persistants
|
||||||
|
|
||||||
### Phase 4 : Pipeline avancé
|
### Phase 4 : Pipeline avancé
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
#include <lardon3d/project_db.h>
|
#include <lardon3d/project_db.h>
|
||||||
|
#include <lardon3d/task_kind_registry.h>
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LARDON3D_PROJECT_TASK_CHECKPOINT_OK = 0,
|
LARDON3D_PROJECT_TASK_CHECKPOINT_OK = 0,
|
||||||
|
|
@ -22,12 +23,17 @@ typedef enum {
|
||||||
LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT,
|
LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT,
|
||||||
LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT,
|
LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT,
|
||||||
LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT,
|
LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT,
|
||||||
LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR
|
LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR,
|
||||||
|
LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED,
|
||||||
|
LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND,
|
||||||
|
LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION
|
||||||
} Lardon3DProjectRecoveryStatus;
|
} Lardon3DProjectRecoveryStatus;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t task_id;
|
uint64_t task_id;
|
||||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
|
||||||
|
uint32_t task_kind_version;
|
||||||
Lardon3DProjectRecoveryStatus status;
|
Lardon3DProjectRecoveryStatus status;
|
||||||
Lardon3DProjectDbCheckpointDurability durability;
|
Lardon3DProjectDbCheckpointDurability durability;
|
||||||
Lardon3DTaskDurableSnapshot snapshot;
|
Lardon3DTaskDurableSnapshot snapshot;
|
||||||
|
|
@ -50,6 +56,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(
|
||||||
);
|
);
|
||||||
Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
uint64_t after_task_id,
|
uint64_t after_task_id,
|
||||||
Lardon3DProjectRecoveryEntry *entries,
|
Lardon3DProjectRecoveryEntry *entries,
|
||||||
size_t capacity,
|
size_t capacity,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
#include <lardon3d/task.h>
|
#include <lardon3d/task.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 1,
|
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 2,
|
||||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||||
|
|
@ -57,6 +57,9 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t task_id;
|
uint64_t task_id;
|
||||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||||
|
bool has_task_kind;
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
|
||||||
|
uint32_t task_kind_version;
|
||||||
Lardon3DTaskState saved_state;
|
Lardon3DTaskState saved_state;
|
||||||
Lardon3DTaskState recovery_state;
|
Lardon3DTaskState recovery_state;
|
||||||
unsigned int progress;
|
unsigned int progress;
|
||||||
|
|
@ -103,6 +106,8 @@ Lardon3DProjectDbResult lardon3d_project_db_get_project(
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_record_task(
|
Lardon3DProjectDbResult lardon3d_project_db_record_task(
|
||||||
Lardon3DProjectDb *database,
|
Lardon3DProjectDb *database,
|
||||||
const Lardon3DTaskDurableSnapshot *snapshot,
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
const char *task_kind,
|
||||||
|
uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint,
|
const Lardon3DProjectDbCheckpoint *checkpoint,
|
||||||
int64_t updated_at
|
int64_t updated_at
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_TASK_NAME_CAPACITY = 128,
|
LARDON3D_TASK_NAME_CAPACITY = 128,
|
||||||
LARDON3D_TASK_MESSAGE_CAPACITY = 256,
|
LARDON3D_TASK_MESSAGE_CAPACITY = 256,
|
||||||
|
LARDON3D_TASK_KIND_CAPACITY = 65,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|
@ -23,6 +24,7 @@ typedef enum {
|
||||||
|
|
||||||
typedef struct Lardon3DTask Lardon3DTask;
|
typedef struct Lardon3DTask Lardon3DTask;
|
||||||
typedef bool (*Lardon3DTaskCallback)(Lardon3DTask *task, void *userdata);
|
typedef bool (*Lardon3DTaskCallback)(Lardon3DTask *task, void *userdata);
|
||||||
|
typedef void (*Lardon3DTaskUserdataDestroy)(void *userdata);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t id;
|
uint64_t id;
|
||||||
|
|
@ -62,6 +64,15 @@ Lardon3DTask *lardon3d_task_create(
|
||||||
Lardon3DTaskCallback callback,
|
Lardon3DTaskCallback callback,
|
||||||
void *userdata
|
void *userdata
|
||||||
);
|
);
|
||||||
|
Lardon3DTask *lardon3d_task_create_typed(
|
||||||
|
const char *name,
|
||||||
|
const Lardon3DResourceEstimate *estimate,
|
||||||
|
const char *task_kind,
|
||||||
|
uint32_t task_kind_version,
|
||||||
|
Lardon3DTaskCallback callback,
|
||||||
|
void *userdata,
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy
|
||||||
|
);
|
||||||
void lardon3d_task_destroy(Lardon3DTask *task);
|
void lardon3d_task_destroy(Lardon3DTask *task);
|
||||||
/* Exécute le callback dans le thread appelant. */
|
/* Exécute le callback dans le thread appelant. */
|
||||||
bool lardon3d_task_start(
|
bool lardon3d_task_start(
|
||||||
|
|
@ -93,6 +104,22 @@ Lardon3DTask *lardon3d_task_restore(
|
||||||
Lardon3DTaskCallback callback,
|
Lardon3DTaskCallback callback,
|
||||||
void *userdata
|
void *userdata
|
||||||
);
|
);
|
||||||
|
Lardon3DTask *lardon3d_task_restore_typed(
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
const char *task_kind,
|
||||||
|
uint32_t task_kind_version,
|
||||||
|
Lardon3DTaskCallback callback,
|
||||||
|
void *userdata,
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy
|
||||||
|
);
|
||||||
|
/* Une restauration typée réussie transfère userdata/userdata_destroy à la
|
||||||
|
* tâche. En cas d'échec, l'appelant en reste propriétaire. */
|
||||||
|
bool lardon3d_task_kind_is_valid(const char *task_kind);
|
||||||
|
bool lardon3d_task_kind(
|
||||||
|
const Lardon3DTask *task,
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY],
|
||||||
|
uint32_t *task_kind_version
|
||||||
|
);
|
||||||
uint64_t lardon3d_task_id(const Lardon3DTask *task);
|
uint64_t lardon3d_task_id(const Lardon3DTask *task);
|
||||||
bool lardon3d_task_assign_id(Lardon3DTask *task, uint64_t id);
|
bool lardon3d_task_assign_id(Lardon3DTask *task, uint64_t id);
|
||||||
bool lardon3d_task_resource_estimate(
|
bool lardon3d_task_resource_estimate(
|
||||||
|
|
|
||||||
65
include/lardon3d/task_kind_registry.h
Normal file
65
include/lardon3d/task_kind_registry.h
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#ifndef LARDON3D_TASK_KIND_REGISTRY_H
|
||||||
|
#define LARDON3D_TASK_KIND_REGISTRY_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <lardon3d/task.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_TASK_KIND_REGISTRY_MAX = 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DTaskCallback callback;
|
||||||
|
void *userdata;
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy;
|
||||||
|
} Lardon3DTaskKindBinding;
|
||||||
|
|
||||||
|
typedef bool (*Lardon3DTaskKindReconstruct)(
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
void *context,
|
||||||
|
Lardon3DTaskKindBinding *binding
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *kind;
|
||||||
|
uint32_t kind_version;
|
||||||
|
Lardon3DTaskKindReconstruct reconstruct;
|
||||||
|
} Lardon3DTaskKindDescriptor;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const Lardon3DTaskKindDescriptor *descriptors;
|
||||||
|
size_t count;
|
||||||
|
} Lardon3DTaskKindRegistry;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_TASK_KIND_OK = 0,
|
||||||
|
LARDON3D_TASK_KIND_INVALID_ARGUMENT,
|
||||||
|
LARDON3D_TASK_KIND_UNKNOWN,
|
||||||
|
LARDON3D_TASK_KIND_UNSUPPORTED_VERSION,
|
||||||
|
LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED,
|
||||||
|
LARDON3D_TASK_KIND_RESTORE_FAILED
|
||||||
|
} Lardon3DTaskKindResult;
|
||||||
|
|
||||||
|
bool lardon3d_task_kind_registry_init(
|
||||||
|
Lardon3DTaskKindRegistry *registry,
|
||||||
|
const Lardon3DTaskKindDescriptor *descriptors,
|
||||||
|
size_t count
|
||||||
|
);
|
||||||
|
Lardon3DTaskKindResult lardon3d_task_kind_registry_lookup(
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
const char *kind,
|
||||||
|
uint32_t kind_version,
|
||||||
|
const Lardon3DTaskKindDescriptor **descriptor
|
||||||
|
);
|
||||||
|
Lardon3DTaskKindResult lardon3d_task_kind_registry_restore(
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
const char *kind,
|
||||||
|
uint32_t kind_version,
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
void *context,
|
||||||
|
Lardon3DTask **task
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
20
meson.build
20
meson.build
|
|
@ -38,6 +38,7 @@ executable(
|
||||||
'src/project_db.c',
|
'src/project_db.c',
|
||||||
'src/task.c',
|
'src/task.c',
|
||||||
'src/task_checkpoint.c',
|
'src/task_checkpoint.c',
|
||||||
|
'src/task_kind_registry.c',
|
||||||
'src/task_queue.c',
|
'src/task_queue.c',
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
|
|
@ -133,6 +134,9 @@ project_db_test = executable(
|
||||||
sources: [
|
sources: [
|
||||||
'tests/test_project_db.c',
|
'tests/test_project_db.c',
|
||||||
'src/project_db.c',
|
'src/project_db.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/resource_governor.c',
|
||||||
|
'src/resource_snapshot.c',
|
||||||
],
|
],
|
||||||
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
|
|
@ -150,6 +154,7 @@ project_test = executable(
|
||||||
'src/project_db.c',
|
'src/project_db.c',
|
||||||
'src/task.c',
|
'src/task.c',
|
||||||
'src/task_checkpoint.c',
|
'src/task_checkpoint.c',
|
||||||
|
'src/task_kind_registry.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',
|
||||||
|
|
@ -165,6 +170,21 @@ project_test = executable(
|
||||||
|
|
||||||
test('project', project_test, timeout: 30)
|
test('project', project_test, timeout: 30)
|
||||||
|
|
||||||
|
task_kind_registry_test = executable(
|
||||||
|
'test-task-kind-registry',
|
||||||
|
sources: [
|
||||||
|
'tests/test_task_kind_registry.c',
|
||||||
|
'src/task_kind_registry.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/resource_governor.c',
|
||||||
|
'src/resource_snapshot.c',
|
||||||
|
],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [threads],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('task-kind-registry', task_kind_registry_test, timeout: 30)
|
||||||
|
|
||||||
sequential_task_test = executable(
|
sequential_task_test = executable(
|
||||||
'test-sequential-task',
|
'test-sequential-task',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
|
||||||
|
|
@ -861,6 +861,11 @@ lardon3d_project_checkpoint_task(
|
||||||
if (!lardon3d_task_durable_snapshot(task, &snapshot) || snapshot.id == 0) {
|
if (!lardon3d_task_durable_snapshot(task, &snapshot) || snapshot.id == 0) {
|
||||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||||
}
|
}
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
|
||||||
|
uint32_t task_kind_version = 0;
|
||||||
|
if (!lardon3d_task_kind(task, task_kind, &task_kind_version)) {
|
||||||
|
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||||
|
}
|
||||||
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
char absolute[PATH_MAX];
|
char absolute[PATH_MAX];
|
||||||
if (!checkpoint_paths(state, snapshot.id, relative, absolute)) {
|
if (!checkpoint_paths(state, snapshot.id, relative, absolute)) {
|
||||||
|
|
@ -892,6 +897,8 @@ lardon3d_project_checkpoint_task(
|
||||||
Lardon3DProjectDbResult recorded = lardon3d_project_db_record_task(
|
Lardon3DProjectDbResult recorded = lardon3d_project_db_record_task(
|
||||||
state->project_db,
|
state->project_db,
|
||||||
&snapshot,
|
&snapshot,
|
||||||
|
task_kind,
|
||||||
|
task_kind_version,
|
||||||
&checkpoint,
|
&checkpoint,
|
||||||
now.tv_sec
|
now.tv_sec
|
||||||
);
|
);
|
||||||
|
|
@ -923,6 +930,7 @@ coherent_recovery(
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
lardon3d_project_list_recoverable(
|
lardon3d_project_list_recoverable(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
uint64_t after_task_id,
|
uint64_t after_task_id,
|
||||||
Lardon3DProjectRecoveryEntry *entries,
|
Lardon3DProjectRecoveryEntry *entries,
|
||||||
size_t capacity,
|
size_t capacity,
|
||||||
|
|
@ -932,7 +940,7 @@ lardon3d_project_list_recoverable(
|
||||||
if (count) {
|
if (count) {
|
||||||
*count = 0;
|
*count = 0;
|
||||||
}
|
}
|
||||||
if (!state || !state->project_loaded || !state->project_db || !entries
|
if (!state || !state->project_loaded || !state->project_db || !registry || !entries
|
||||||
|| !count || capacity == 0
|
|| !count || capacity == 0
|
||||||
|| capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) {
|
|| capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) {
|
||||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
|
@ -962,9 +970,28 @@ lardon3d_project_list_recoverable(
|
||||||
entry->task_id = tasks[index].task_id;
|
entry->task_id = tasks[index].task_id;
|
||||||
entry->durability = tasks[index].checkpoint.durability;
|
entry->durability = tasks[index].checkpoint.durability;
|
||||||
(void)snprintf(entry->name, sizeof(entry->name), "%s", tasks[index].name);
|
(void)snprintf(entry->name, sizeof(entry->name), "%s", tasks[index].name);
|
||||||
|
if (tasks[index].has_task_kind) {
|
||||||
|
(void)snprintf(entry->task_kind, sizeof(entry->task_kind), "%s",
|
||||||
|
tasks[index].task_kind);
|
||||||
|
entry->task_kind_version = tasks[index].task_kind_version;
|
||||||
|
}
|
||||||
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
char absolute[PATH_MAX];
|
char absolute[PATH_MAX];
|
||||||
if (!checkpoint_paths(state, entry->task_id, relative, absolute)
|
const Lardon3DTaskKindDescriptor *descriptor = NULL;
|
||||||
|
Lardon3DTaskKindResult kind_result = tasks[index].has_task_kind
|
||||||
|
? lardon3d_task_kind_registry_lookup(
|
||||||
|
registry, tasks[index].task_kind,
|
||||||
|
tasks[index].task_kind_version, &descriptor)
|
||||||
|
: LARDON3D_TASK_KIND_UNKNOWN;
|
||||||
|
if (!tasks[index].has_task_kind) {
|
||||||
|
entry->status = LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED;
|
||||||
|
} else if (kind_result == LARDON3D_TASK_KIND_UNKNOWN) {
|
||||||
|
entry->status = LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND;
|
||||||
|
} else if (kind_result == LARDON3D_TASK_KIND_UNSUPPORTED_VERSION) {
|
||||||
|
entry->status = LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION;
|
||||||
|
} else if (kind_result != LARDON3D_TASK_KIND_OK) {
|
||||||
|
entry->status = LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND;
|
||||||
|
} else if (!checkpoint_paths(state, entry->task_id, relative, absolute)
|
||||||
|| strcmp(relative, tasks[index].checkpoint.path) != 0) {
|
|| strcmp(relative, tasks[index].checkpoint.path) != 0) {
|
||||||
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
|
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
120
src/project_db.c
120
src/project_db.c
|
|
@ -17,17 +17,19 @@ struct Lardon3DProjectDb {
|
||||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char schema_v1[] =
|
static const char schema_v2[] =
|
||||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
"INSERT INTO metadata(key,value) VALUES('schema_version',1);"
|
"INSERT INTO metadata(key,value) VALUES('schema_version',2);"
|
||||||
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
|
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
|
||||||
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||||
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
|
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
|
||||||
|
"task_kind TEXT,task_kind_version INTEGER,"
|
||||||
"saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),"
|
"saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),"
|
||||||
"recovery_state INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),"
|
"recovery_state INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),"
|
||||||
"progress INTEGER NOT NULL CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL CHECK(sequence_count>=0),"
|
"progress INTEGER NOT NULL CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL CHECK(sequence_count>=0),"
|
||||||
"started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL CHECK(started_nsec BETWEEN 0 AND 999999999),"
|
"started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL CHECK(started_nsec BETWEEN 0 AND 999999999),"
|
||||||
"finished_sec INTEGER NOT NULL,finished_nsec INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND 999999999),updated_at INTEGER NOT NULL);"
|
"finished_sec INTEGER NOT NULL,finished_nsec INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND 999999999),updated_at INTEGER NOT NULL,"
|
||||||
|
"CHECK((task_kind IS NULL AND task_kind_version IS NULL) OR (task_kind IS NOT NULL AND task_kind_version>0)));"
|
||||||
"CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);"
|
"CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);"
|
||||||
"CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
"CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
||||||
"path TEXT NOT NULL,format_version INTEGER NOT NULL CHECK(format_version>0),"
|
"path TEXT NOT NULL,format_version INTEGER NOT NULL CHECK(format_version>0),"
|
||||||
|
|
@ -122,10 +124,35 @@ migrate(Lardon3DProjectDb *database, unsigned int from_version)
|
||||||
copy_error(database->error, "Version de schéma future non supportée.");
|
copy_error(database->error, "Version de schéma future non supportée.");
|
||||||
return LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA;
|
return LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA;
|
||||||
}
|
}
|
||||||
if (from_version == 1) return LARDON3D_PROJECT_DB_OK;
|
if (from_version == LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
|
||||||
if (from_version != 0) return LARDON3D_PROJECT_DB_CORRUPT;
|
return LARDON3D_PROJECT_DB_OK;
|
||||||
|
}
|
||||||
|
if (from_version != 0 && from_version != 1) {
|
||||||
|
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
}
|
||||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, schema_v1, "create schema v1");
|
if (result == LARDON3D_PROJECT_DB_OK && from_version == 0) {
|
||||||
|
result = execute(database, schema_v2, "create schema v2");
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && from_version == 1) {
|
||||||
|
result = execute(database,
|
||||||
|
"ALTER TABLE tasks ADD COLUMN task_kind TEXT;"
|
||||||
|
"ALTER TABLE tasks ADD COLUMN task_kind_version INTEGER CHECK(task_kind_version IS NULL OR task_kind_version>0)",
|
||||||
|
"migrate schema v1 to v2");
|
||||||
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
|
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2");
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && forced_failure
|
||||||
|
&& strcmp(forced_failure, "1") == 0) {
|
||||||
|
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
|
||||||
|
"forced migration failure");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
result = execute(database,
|
||||||
|
"UPDATE metadata SET value=2 WHERE key='schema_version' AND value=1",
|
||||||
|
"finish schema v2 migration");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit migration");
|
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit migration");
|
||||||
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback migration");
|
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback migration");
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -187,7 +214,7 @@ lardon3d_project_db_open(const char *path, Lardon3DProjectDb **output, char erro
|
||||||
unsigned int version = 0;
|
unsigned int version = 0;
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = read_schema_version(database, &version);
|
if (result == LARDON3D_PROJECT_DB_OK) result = read_schema_version(database, &version);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = migrate(database, version);
|
if (result == LARDON3D_PROJECT_DB_OK) result = migrate(database, version);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && version == 1) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
const char *required[] = {"project", "tasks", "checkpoints", "artifacts"};
|
const char *required[] = {"project", "tasks", "checkpoints", "artifacts"};
|
||||||
for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
||||||
if (!table_exists(database->connection, required[index])) {
|
if (!table_exists(database->connection, required[index])) {
|
||||||
|
|
@ -223,7 +250,7 @@ lardon3d_project_db_last_error(Lardon3DProjectDb *database, char error[LARDON3D_
|
||||||
(void)pthread_mutex_unlock(&database->mutex);
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) { return database ? 1U : 0U; }
|
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) { return database ? LARDON3D_PROJECT_DB_SCHEMA_VERSION : 0U; }
|
||||||
|
|
||||||
static Lardon3DProjectDbResult
|
static Lardon3DProjectDbResult
|
||||||
step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context)
|
step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context)
|
||||||
|
|
@ -323,9 +350,15 @@ valid_durable_task(const Lardon3DTaskDurableSnapshot *snapshot, int64_t updated_
|
||||||
|
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
lardon3d_project_db_record_task(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
lardon3d_project_db_record_task(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
const char *task_kind, uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at)
|
const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at)
|
||||||
{
|
{
|
||||||
if (!database || !valid_durable_task(snapshot, updated_at) || (checkpoint && !valid_checkpoint(checkpoint))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
bool typed = task_kind != NULL;
|
||||||
|
if (!database || !valid_durable_task(snapshot, updated_at)
|
||||||
|
|| (typed && (!lardon3d_task_kind_is_valid(task_kind)
|
||||||
|
|| task_kind_version == 0))
|
||||||
|
|| (!typed && task_kind_version != 0)
|
||||||
|
|| (checkpoint && !valid_checkpoint(checkpoint))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
|
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
|
||||||
if (forced_busy && checkpoint && strcmp(forced_busy, "1") == 0) {
|
if (forced_busy && checkpoint && strcmp(forced_busy, "1") == 0) {
|
||||||
|
|
@ -337,17 +370,29 @@ lardon3d_project_db_record_task(Lardon3DProjectDb *database, const Lardon3DTaskD
|
||||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin task record");
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin task record");
|
||||||
sqlite3_stmt *statement = NULL;
|
sqlite3_stmt *statement = NULL;
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||||
"INSERT INTO tasks(task_id,name,saved_state,recovery_state,progress,sequence_count,started_sec,started_nsec,finished_sec,finished_nsec,updated_at)"
|
"INSERT INTO tasks(task_id,name,task_kind,task_kind_version,saved_state,recovery_state,progress,sequence_count,started_sec,started_nsec,finished_sec,finished_nsec,updated_at)"
|
||||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11) ON CONFLICT(task_id) DO UPDATE SET name=excluded.name,saved_state=excluded.saved_state,"
|
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13) ON CONFLICT(task_id) DO UPDATE SET name=excluded.name,saved_state=excluded.saved_state,"
|
||||||
"recovery_state=excluded.recovery_state,progress=excluded.progress,sequence_count=excluded.sequence_count,started_sec=excluded.started_sec,"
|
"recovery_state=excluded.recovery_state,progress=excluded.progress,sequence_count=excluded.sequence_count,started_sec=excluded.started_sec,"
|
||||||
"started_nsec=excluded.started_nsec,finished_sec=excluded.finished_sec,finished_nsec=excluded.finished_nsec,updated_at=excluded.updated_at", &statement);
|
"started_nsec=excluded.started_nsec,finished_sec=excluded.finished_sec,finished_nsec=excluded.finished_nsec,updated_at=excluded.updated_at "
|
||||||
|
"WHERE (tasks.task_kind IS NULL AND excluded.task_kind IS NULL) OR (tasks.task_kind=excluded.task_kind AND tasks.task_kind_version=excluded.task_kind_version)", &statement);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id); (void)sqlite3_bind_text(statement, 2, snapshot->name, -1, SQLITE_TRANSIENT);
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id); (void)sqlite3_bind_text(statement, 2, snapshot->name, -1, SQLITE_TRANSIENT);
|
||||||
(void)sqlite3_bind_int(statement, 3, (int)snapshot->saved_state); (void)sqlite3_bind_int(statement, 4, (int)snapshot->recovery_state);
|
if (typed) {
|
||||||
(void)sqlite3_bind_int(statement, 5, (int)snapshot->progress); (void)sqlite3_bind_int(statement, 6, (int)snapshot->sequence_count);
|
(void)sqlite3_bind_text(statement, 3, task_kind, -1, SQLITE_TRANSIENT);
|
||||||
(void)sqlite3_bind_int64(statement, 7, snapshot->started_at.tv_sec); (void)sqlite3_bind_int64(statement, 8, snapshot->started_at.tv_nsec);
|
(void)sqlite3_bind_int64(statement, 4, task_kind_version);
|
||||||
(void)sqlite3_bind_int64(statement, 9, snapshot->finished_at.tv_sec); (void)sqlite3_bind_int64(statement, 10, snapshot->finished_at.tv_nsec);
|
} else {
|
||||||
(void)sqlite3_bind_int64(statement, 11, updated_at); result = step_done(database, statement, "upsert task");
|
(void)sqlite3_bind_null(statement, 3); (void)sqlite3_bind_null(statement, 4);
|
||||||
|
}
|
||||||
|
(void)sqlite3_bind_int(statement, 5, (int)snapshot->saved_state); (void)sqlite3_bind_int(statement, 6, (int)snapshot->recovery_state);
|
||||||
|
(void)sqlite3_bind_int(statement, 7, (int)snapshot->progress); (void)sqlite3_bind_int(statement, 8, (int)snapshot->sequence_count);
|
||||||
|
(void)sqlite3_bind_int64(statement, 9, snapshot->started_at.tv_sec); (void)sqlite3_bind_int64(statement, 10, snapshot->started_at.tv_nsec);
|
||||||
|
(void)sqlite3_bind_int64(statement, 11, snapshot->finished_at.tv_sec); (void)sqlite3_bind_int64(statement, 12, snapshot->finished_at.tv_nsec);
|
||||||
|
(void)sqlite3_bind_int64(statement, 13, updated_at); result = step_done(database, statement, "upsert task");
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& sqlite3_changes(database->connection) != 1) {
|
||||||
|
copy_error(database->error, "Type métier de tâche immuable.");
|
||||||
|
result = LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
|
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
|
||||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
|
|
@ -377,34 +422,45 @@ read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task)
|
||||||
{
|
{
|
||||||
memset(task, 0, sizeof(*task));
|
memset(task, 0, sizeof(*task));
|
||||||
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||||
int progress = sqlite3_column_int(statement, 4);
|
bool has_kind = sqlite3_column_type(statement, 2) != SQLITE_NULL;
|
||||||
sqlite3_int64 sequence_count = sqlite3_column_int64(statement, 5);
|
bool has_kind_version = sqlite3_column_type(statement, 3) != SQLITE_NULL;
|
||||||
sqlite3_int64 started_nsec = sqlite3_column_int64(statement, 7);
|
int progress = sqlite3_column_int(statement, 6);
|
||||||
sqlite3_int64 finished_nsec = sqlite3_column_int64(statement, 9);
|
sqlite3_int64 sequence_count = sqlite3_column_int64(statement, 7);
|
||||||
|
sqlite3_int64 started_nsec = sqlite3_column_int64(statement, 9);
|
||||||
|
sqlite3_int64 finished_nsec = sqlite3_column_int64(statement, 11);
|
||||||
if (id <= 0 || !copy_column(statement, 1, task->name, sizeof(task->name))) return false;
|
if (id <= 0 || !copy_column(statement, 1, task->name, sizeof(task->name))) return false;
|
||||||
task->task_id = (uint64_t)id; task->saved_state = (Lardon3DTaskState)sqlite3_column_int(statement, 2);
|
if (has_kind != has_kind_version) return false;
|
||||||
task->recovery_state = (Lardon3DTaskState)sqlite3_column_int(statement, 3);
|
if (has_kind) {
|
||||||
|
sqlite3_int64 version = sqlite3_column_int64(statement, 3);
|
||||||
|
if (!copy_column(statement, 2, task->task_kind, sizeof(task->task_kind))
|
||||||
|
|| !lardon3d_task_kind_is_valid(task->task_kind)
|
||||||
|
|| version <= 0 || version > UINT32_MAX) return false;
|
||||||
|
task->has_task_kind = true;
|
||||||
|
task->task_kind_version = (uint32_t)version;
|
||||||
|
}
|
||||||
|
task->task_id = (uint64_t)id; task->saved_state = (Lardon3DTaskState)sqlite3_column_int(statement, 4);
|
||||||
|
task->recovery_state = (Lardon3DTaskState)sqlite3_column_int(statement, 5);
|
||||||
if (progress < 0 || progress > 100 || sequence_count < 0 || sequence_count > UINT_MAX
|
if (progress < 0 || progress > 100 || sequence_count < 0 || sequence_count > UINT_MAX
|
||||||
|| started_nsec < 0 || started_nsec >= 1000000000 || finished_nsec < 0 || finished_nsec >= 1000000000
|
|| started_nsec < 0 || started_nsec >= 1000000000 || finished_nsec < 0 || finished_nsec >= 1000000000
|
||||||
|| !database_time(sqlite3_column_int64(statement, 6), &task->started_at.tv_sec)
|
|| !database_time(sqlite3_column_int64(statement, 8), &task->started_at.tv_sec)
|
||||||
|| !database_time(sqlite3_column_int64(statement, 8), &task->finished_at.tv_sec)) return false;
|
|| !database_time(sqlite3_column_int64(statement, 10), &task->finished_at.tv_sec)) return false;
|
||||||
task->progress = (unsigned int)progress; task->sequence_count = (unsigned int)sequence_count;
|
task->progress = (unsigned int)progress; task->sequence_count = (unsigned int)sequence_count;
|
||||||
task->started_at.tv_nsec = (long)started_nsec; task->finished_at.tv_nsec = (long)finished_nsec; task->updated_at = sqlite3_column_int64(statement, 10);
|
task->started_at.tv_nsec = (long)started_nsec; task->finished_at.tv_nsec = (long)finished_nsec; task->updated_at = sqlite3_column_int64(statement, 12);
|
||||||
if (sqlite3_column_type(statement, 11) != SQLITE_NULL) {
|
if (sqlite3_column_type(statement, 13) != SQLITE_NULL) {
|
||||||
task->has_checkpoint = true;
|
task->has_checkpoint = true;
|
||||||
if (!copy_column(statement, 11, task->checkpoint.path, sizeof(task->checkpoint.path))) return false;
|
if (!copy_column(statement, 13, task->checkpoint.path, sizeof(task->checkpoint.path))) return false;
|
||||||
sqlite3_int64 format_version = sqlite3_column_int64(statement, 12);
|
sqlite3_int64 format_version = sqlite3_column_int64(statement, 14);
|
||||||
if (format_version <= 0 || format_version > UINT32_MAX) return false;
|
if (format_version <= 0 || format_version > UINT32_MAX) return false;
|
||||||
task->checkpoint.format_version = (uint32_t)format_version;
|
task->checkpoint.format_version = (uint32_t)format_version;
|
||||||
task->checkpoint.durability = (Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 13);
|
task->checkpoint.durability = (Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 15);
|
||||||
task->checkpoint.updated_at = sqlite3_column_int64(statement, 14);
|
task->checkpoint.updated_at = sqlite3_column_int64(statement, 16);
|
||||||
if (task->checkpoint.durability < LARDON3D_DB_CHECKPOINT_DURABLE
|
if (task->checkpoint.durability < LARDON3D_DB_CHECKPOINT_DURABLE
|
||||||
|| task->checkpoint.durability > LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE) return false;
|
|| task->checkpoint.durability > LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE) return false;
|
||||||
}
|
}
|
||||||
return valid_state(task->saved_state) && valid_state(task->recovery_state) && task->progress <= 100;
|
return valid_state(task->saved_state) && valid_state(task->recovery_state) && task->progress <= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char task_select[] = "SELECT t.task_id,t.name,t.saved_state,t.recovery_state,t.progress,t.sequence_count,t.started_sec,t.started_nsec,"
|
static const char task_select[] = "SELECT t.task_id,t.name,t.task_kind,t.task_kind_version,t.saved_state,t.recovery_state,t.progress,t.sequence_count,t.started_sec,t.started_nsec,"
|
||||||
"t.finished_sec,t.finished_nsec,t.updated_at,c.path,c.format_version,c.durability,c.updated_at FROM tasks t LEFT JOIN checkpoints c ON c.task_id=t.task_id ";
|
"t.finished_sec,t.finished_nsec,t.updated_at,c.path,c.format_version,c.durability,c.updated_at FROM tasks t LEFT JOIN checkpoints c ON c.task_id=t.task_id ";
|
||||||
|
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
|
|
|
||||||
101
src/task.c
101
src/task.c
|
|
@ -18,6 +18,9 @@ struct Lardon3DTask {
|
||||||
struct timespec finished_at;
|
struct timespec finished_at;
|
||||||
Lardon3DTaskCallback callback;
|
Lardon3DTaskCallback callback;
|
||||||
void *userdata;
|
void *userdata;
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy;
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
|
||||||
|
uint32_t task_kind_version;
|
||||||
Lardon3DResourceEstimate estimate;
|
Lardon3DResourceEstimate estimate;
|
||||||
Lardon3DTaskExecutionContract contract;
|
Lardon3DTaskExecutionContract contract;
|
||||||
bool has_contract;
|
bool has_contract;
|
||||||
|
|
@ -42,6 +45,28 @@ valid_state(Lardon3DTaskState state)
|
||||||
return state >= TASK_PENDING && state <= TASK_COMPLETED;
|
return state >= TASK_PENDING && state <= TASK_COMPLETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_task_kind_is_valid(const char *task_kind)
|
||||||
|
{
|
||||||
|
if (!task_kind) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t length = strnlen(task_kind, LARDON3D_TASK_KIND_CAPACITY);
|
||||||
|
if (length == 0 || length >= LARDON3D_TASK_KIND_CAPACITY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < length; ++index) {
|
||||||
|
char character = task_kind[index];
|
||||||
|
if (!((character >= 'a' && character <= 'z')
|
||||||
|
|| (character >= '0' && character <= '9')
|
||||||
|
|| (index > 0 && (character == '.' || character == '_'
|
||||||
|
|| character == '-')))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static Lardon3DTaskState
|
static Lardon3DTaskState
|
||||||
recovery_state(Lardon3DTaskState state)
|
recovery_state(Lardon3DTaskState state)
|
||||||
{
|
{
|
||||||
|
|
@ -81,7 +106,27 @@ lardon3d_task_create(
|
||||||
void *userdata
|
void *userdata
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!name || !name[0] || !estimate || !callback) {
|
return lardon3d_task_create_typed(
|
||||||
|
name, estimate, NULL, 0, callback, userdata, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DTask *
|
||||||
|
lardon3d_task_create_typed(
|
||||||
|
const char *name,
|
||||||
|
const Lardon3DResourceEstimate *estimate,
|
||||||
|
const char *task_kind,
|
||||||
|
uint32_t task_kind_version,
|
||||||
|
Lardon3DTaskCallback callback,
|
||||||
|
void *userdata,
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy
|
||||||
|
)
|
||||||
|
{
|
||||||
|
bool typed = task_kind != NULL;
|
||||||
|
if (!name || !name[0] || !estimate || !callback
|
||||||
|
|| (typed && (!lardon3d_task_kind_is_valid(task_kind)
|
||||||
|
|| task_kind_version == 0))
|
||||||
|
|| (!typed && (task_kind_version != 0 || userdata_destroy))) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
Lardon3DTask *task = calloc(1, sizeof(*task));
|
Lardon3DTask *task = calloc(1, sizeof(*task));
|
||||||
|
|
@ -102,6 +147,11 @@ lardon3d_task_create(
|
||||||
task->state = TASK_PENDING;
|
task->state = TASK_PENDING;
|
||||||
task->callback = callback;
|
task->callback = callback;
|
||||||
task->userdata = userdata;
|
task->userdata = userdata;
|
||||||
|
task->userdata_destroy = userdata_destroy;
|
||||||
|
if (typed) {
|
||||||
|
(void)snprintf(task->task_kind, sizeof(task->task_kind), "%s", task_kind);
|
||||||
|
task->task_kind_version = task_kind_version;
|
||||||
|
}
|
||||||
task->estimate = *estimate;
|
task->estimate = *estimate;
|
||||||
copy_text(task->message, sizeof(task->message), "En attente.");
|
copy_text(task->message, sizeof(task->message), "En attente.");
|
||||||
return task;
|
return task;
|
||||||
|
|
@ -117,6 +167,9 @@ lardon3d_task_destroy(Lardon3DTask *task)
|
||||||
(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);
|
||||||
|
if (task->userdata_destroy) {
|
||||||
|
task->userdata_destroy(task->userdata);
|
||||||
|
}
|
||||||
free(task);
|
free(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,6 +658,21 @@ lardon3d_task_restore(
|
||||||
Lardon3DTaskCallback callback,
|
Lardon3DTaskCallback callback,
|
||||||
void *userdata
|
void *userdata
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
return lardon3d_task_restore_typed(
|
||||||
|
snapshot, NULL, 0, callback, userdata, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DTask *
|
||||||
|
lardon3d_task_restore_typed(
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
const char *task_kind,
|
||||||
|
uint32_t task_kind_version,
|
||||||
|
Lardon3DTaskCallback callback,
|
||||||
|
void *userdata,
|
||||||
|
Lardon3DTaskUserdataDestroy userdata_destroy
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!snapshot || snapshot->id == 0 || !snapshot->name[0] || !callback
|
if (!snapshot || snapshot->id == 0 || !snapshot->name[0] || !callback
|
||||||
|| memchr(snapshot->name, '\0', sizeof(snapshot->name)) == NULL
|
|| memchr(snapshot->name, '\0', sizeof(snapshot->name)) == NULL
|
||||||
|
|
@ -625,11 +693,14 @@ lardon3d_task_restore(
|
||||||
&& snapshot->estimate.desired_gpu_slots == 0)) {
|
&& snapshot->estimate.desired_gpu_slots == 0)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
Lardon3DTask *task = lardon3d_task_create(
|
Lardon3DTask *task = lardon3d_task_create_typed(
|
||||||
snapshot->name,
|
snapshot->name,
|
||||||
&snapshot->estimate,
|
&snapshot->estimate,
|
||||||
|
task_kind,
|
||||||
|
task_kind_version,
|
||||||
callback,
|
callback,
|
||||||
userdata
|
userdata,
|
||||||
|
userdata_destroy
|
||||||
);
|
);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -646,6 +717,30 @@ lardon3d_task_restore(
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_task_kind(
|
||||||
|
const Lardon3DTask *task,
|
||||||
|
char task_kind[LARDON3D_TASK_KIND_CAPACITY],
|
||||||
|
uint32_t *task_kind_version
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!task || !task_kind || !task_kind_version) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Lardon3DTask *mutable_task = (Lardon3DTask *)task;
|
||||||
|
(void)pthread_mutex_lock(&mutable_task->mutex);
|
||||||
|
bool typed = task->task_kind[0] != '\0';
|
||||||
|
if (typed) {
|
||||||
|
copy_text(task_kind, LARDON3D_TASK_KIND_CAPACITY, task->task_kind);
|
||||||
|
*task_kind_version = task->task_kind_version;
|
||||||
|
} else {
|
||||||
|
task_kind[0] = '\0';
|
||||||
|
*task_kind_version = 0;
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&mutable_task->mutex);
|
||||||
|
return typed;
|
||||||
|
}
|
||||||
|
|
||||||
uint64_t
|
uint64_t
|
||||||
lardon3d_task_id(const Lardon3DTask *task)
|
lardon3d_task_id(const Lardon3DTask *task)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
116
src/task_kind_registry.c
Normal file
116
src/task_kind_registry.c
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <lardon3d/task_kind_registry.h>
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_task_kind_registry_init(
|
||||||
|
Lardon3DTaskKindRegistry *registry,
|
||||||
|
const Lardon3DTaskKindDescriptor *descriptors,
|
||||||
|
size_t count
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!registry || count > LARDON3D_TASK_KIND_REGISTRY_MAX
|
||||||
|
|| (count > 0 && !descriptors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < count; ++index) {
|
||||||
|
if (!lardon3d_task_kind_is_valid(descriptors[index].kind)
|
||||||
|
|| descriptors[index].kind_version == 0
|
||||||
|
|| !descriptors[index].reconstruct) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (size_t previous = 0; previous < index; ++previous) {
|
||||||
|
if (strcmp(descriptors[index].kind,
|
||||||
|
descriptors[previous].kind) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*registry = (Lardon3DTaskKindRegistry) {
|
||||||
|
.descriptors = descriptors,
|
||||||
|
.count = count,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DTaskKindResult
|
||||||
|
lardon3d_task_kind_registry_lookup(
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
const char *kind,
|
||||||
|
uint32_t kind_version,
|
||||||
|
const Lardon3DTaskKindDescriptor **descriptor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (descriptor) {
|
||||||
|
*descriptor = NULL;
|
||||||
|
}
|
||||||
|
if (!registry || !descriptor || !lardon3d_task_kind_is_valid(kind)
|
||||||
|
|| kind_version == 0 || registry->count > LARDON3D_TASK_KIND_REGISTRY_MAX
|
||||||
|
|| (registry->count > 0 && !registry->descriptors)) {
|
||||||
|
return LARDON3D_TASK_KIND_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
bool kind_known = false;
|
||||||
|
for (size_t index = 0; index < registry->count; ++index) {
|
||||||
|
const Lardon3DTaskKindDescriptor *candidate =
|
||||||
|
®istry->descriptors[index];
|
||||||
|
if (strcmp(candidate->kind, kind) == 0) {
|
||||||
|
kind_known = true;
|
||||||
|
if (candidate->kind_version == kind_version) {
|
||||||
|
*descriptor = candidate;
|
||||||
|
return LARDON3D_TASK_KIND_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kind_known ? LARDON3D_TASK_KIND_UNSUPPORTED_VERSION
|
||||||
|
: LARDON3D_TASK_KIND_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DTaskKindResult
|
||||||
|
lardon3d_task_kind_registry_restore(
|
||||||
|
const Lardon3DTaskKindRegistry *registry,
|
||||||
|
const char *kind,
|
||||||
|
uint32_t kind_version,
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
void *context,
|
||||||
|
Lardon3DTask **task
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (task) {
|
||||||
|
*task = NULL;
|
||||||
|
}
|
||||||
|
if (!snapshot || !task) {
|
||||||
|
return LARDON3D_TASK_KIND_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
const Lardon3DTaskKindDescriptor *descriptor = NULL;
|
||||||
|
Lardon3DTaskKindResult result = lardon3d_task_kind_registry_lookup(
|
||||||
|
registry, kind, kind_version, &descriptor
|
||||||
|
);
|
||||||
|
if (result != LARDON3D_TASK_KIND_OK) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Lardon3DTaskKindBinding binding = {0};
|
||||||
|
if (!descriptor->reconstruct(snapshot, context, &binding)
|
||||||
|
|| !binding.callback) {
|
||||||
|
if (binding.userdata_destroy) {
|
||||||
|
binding.userdata_destroy(binding.userdata);
|
||||||
|
}
|
||||||
|
return LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED;
|
||||||
|
}
|
||||||
|
*task = lardon3d_task_restore_typed(
|
||||||
|
snapshot,
|
||||||
|
kind,
|
||||||
|
kind_version,
|
||||||
|
binding.callback,
|
||||||
|
binding.userdata,
|
||||||
|
binding.userdata_destroy
|
||||||
|
);
|
||||||
|
if (!*task) {
|
||||||
|
if (binding.userdata_destroy) {
|
||||||
|
binding.userdata_destroy(binding.userdata);
|
||||||
|
}
|
||||||
|
return LARDON3D_TASK_KIND_RESTORE_FAILED;
|
||||||
|
}
|
||||||
|
return LARDON3D_TASK_KIND_OK;
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,25 @@ unused_callback(Lardon3DTask *task, void *userdata)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
reconstruct_test_task(
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
void *context,
|
||||||
|
Lardon3DTaskKindBinding *binding
|
||||||
|
)
|
||||||
|
{
|
||||||
|
(void)snapshot;
|
||||||
|
binding->callback = unused_callback;
|
||||||
|
binding->userdata = context;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Lardon3DTaskKindDescriptor test_descriptors[] = {{
|
||||||
|
.kind = "test.persisted",
|
||||||
|
.kind_version = 1,
|
||||||
|
.reconstruct = reconstruct_test_task,
|
||||||
|
}};
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
write_ini(const char *path, const char *name, const char *stable_id, unsigned int version)
|
write_ini(const char *path, const char *name, const char *stable_id, unsigned int version)
|
||||||
{
|
{
|
||||||
|
|
@ -56,6 +75,8 @@ run_test(void)
|
||||||
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
||||||
Lardon3DAppState state;
|
Lardon3DAppState state;
|
||||||
lardon3d_app_state_init(&state);
|
lardon3d_app_state_init(&state);
|
||||||
|
Lardon3DTaskKindRegistry registry;
|
||||||
|
CHECK(lardon3d_task_kind_registry_init(®istry, test_descriptors, 1));
|
||||||
CHECK(lardon3d_project_create(&state, "Projet Cycle"));
|
CHECK(lardon3d_project_create(&state, "Projet Cycle"));
|
||||||
CHECK(state.project_loaded && state.project_db && strlen(state.project_stable_id) == 32);
|
CHECK(state.project_loaded && state.project_db && strlen(state.project_stable_id) == 32);
|
||||||
|
|
||||||
|
|
@ -97,7 +118,9 @@ run_test(void)
|
||||||
.maximum_batch_size = 1,
|
.maximum_batch_size = 1,
|
||||||
.desired_cpu_threads = 1,
|
.desired_cpu_threads = 1,
|
||||||
};
|
};
|
||||||
Lardon3DTask *task = lardon3d_task_create("Persistée", &estimate, unused_callback, NULL);
|
Lardon3DTask *task = lardon3d_task_create_typed(
|
||||||
|
"Persistée", &estimate, "test.persisted", 1,
|
||||||
|
unused_callback, NULL, NULL);
|
||||||
CHECK(task && lardon3d_task_assign_id(task, 1));
|
CHECK(task && lardon3d_task_assign_id(task, 1));
|
||||||
CHECK(lardon3d_task_set_progress(task, 10, "frontière 10"));
|
CHECK(lardon3d_task_set_progress(task, 10, "frontière 10"));
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
|
@ -113,7 +136,7 @@ run_test(void)
|
||||||
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE") == 0);
|
||||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(db_task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
CHECK(db_task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 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_PUBLISHED_NOT_DURABLE);
|
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE);
|
||||||
|
|
||||||
CHECK(lardon3d_task_set_progress(task, 20, "frontière 20"));
|
CHECK(lardon3d_task_set_progress(task, 20, "frontière 20"));
|
||||||
|
|
@ -121,7 +144,9 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE") == 0);
|
||||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
||||||
Lardon3DTask *unpublished = lardon3d_task_create("Non publiée", &estimate, unused_callback, NULL);
|
Lardon3DTask *unpublished = lardon3d_task_create_typed(
|
||||||
|
"Non publiée", &estimate, "test.persisted", 1,
|
||||||
|
unused_callback, NULL, NULL);
|
||||||
CHECK(unpublished && lardon3d_task_assign_id(unpublished, 3));
|
CHECK(unpublished && lardon3d_task_assign_id(unpublished, 3));
|
||||||
CHECK(setenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE", "1", 1) == 0);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, unpublished) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
CHECK(lardon3d_project_checkpoint_task(&state, unpublished) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
||||||
|
|
@ -146,25 +171,35 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 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 && entries[0].snapshot.progress == 20);
|
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE && entries[0].snapshot.progress == 20);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 257, &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 257, &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
|
|
||||||
CHECK(unlink(checkpoint_path) == 0);
|
CHECK(unlink(checkpoint_path) == 0);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 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_RECOVERY_MISSING_CHECKPOINT);
|
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
int descriptor = open(checkpoint_path, O_WRONLY | O_TRUNC); CHECK(descriptor >= 0);
|
int descriptor = open(checkpoint_path, O_WRONLY | O_TRUNC); CHECK(descriptor >= 0);
|
||||||
CHECK(write(descriptor, "corrupt", 7) == 7 && close(descriptor) == 0);
|
CHECK(write(descriptor, "corrupt", 7) == 7 && close(descriptor) == 0);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT);
|
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
descriptor = open(checkpoint_path, O_RDWR); CHECK(descriptor >= 0);
|
descriptor = open(checkpoint_path, O_RDWR); CHECK(descriptor >= 0);
|
||||||
unsigned char future_version[4] = {2, 0, 0, 0};
|
unsigned char future_version[4] = {2, 0, 0, 0};
|
||||||
CHECK(pwrite(descriptor, future_version, sizeof(future_version), 8) == 4 && close(descriptor) == 0);
|
CHECK(pwrite(descriptor, future_version, sizeof(future_version), 8) == 4 && close(descriptor) == 0);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT);
|
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
CHECK(lardon3d_task_checkpoint_load(checkpoint_path, &disk_snapshot, NULL)
|
||||||
|
== LARDON3D_TASK_CHECKPOINT_OK);
|
||||||
|
disk_snapshot.id = 99;
|
||||||
|
CHECK(lardon3d_task_checkpoint_save(checkpoint_path, &disk_snapshot)
|
||||||
|
== LARDON3D_TASK_CHECKPOINT_OK);
|
||||||
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2,
|
||||||
|
&count) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT);
|
||||||
|
CHECK(lardon3d_project_checkpoint_task(&state, task)
|
||||||
|
== LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
|
||||||
CheckpointThread contexts[2] = {{.state = &state, .task = task}, {.state = &state, .task = task}};
|
CheckpointThread contexts[2] = {{.state = &state, .task = task}, {.state = &state, .task = task}};
|
||||||
pthread_t threads[2]; CHECK(pthread_create(&threads[0], NULL, checkpoint_thread, &contexts[0]) == 0);
|
pthread_t threads[2]; CHECK(pthread_create(&threads[0], NULL, checkpoint_thread, &contexts[0]) == 0);
|
||||||
|
|
@ -172,17 +207,77 @@ run_test(void)
|
||||||
CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0);
|
CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0);
|
||||||
CHECK(contexts[0].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK && contexts[1].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(contexts[0].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK && contexts[1].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
|
||||||
Lardon3DTask *terminal = lardon3d_task_create("Terminale", &estimate, unused_callback, NULL);
|
Lardon3DTask *terminal = lardon3d_task_create_typed(
|
||||||
|
"Terminale", &estimate, "test.persisted", 1,
|
||||||
|
unused_callback, NULL, NULL);
|
||||||
CHECK(terminal && lardon3d_task_assign_id(terminal, 2)); lardon3d_task_request_cancel(terminal);
|
CHECK(terminal && lardon3d_task_assign_id(terminal, 2)); lardon3d_task_request_cancel(terminal);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, terminal) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
CHECK(lardon3d_project_checkpoint_task(&state, terminal) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK && count == 1);
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK && count == 1);
|
||||||
lardon3d_task_destroy(terminal);
|
lardon3d_task_destroy(terminal);
|
||||||
|
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT);
|
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT);
|
||||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||||
CHECK(lardon3d_project_list_recoverable(&state, 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);
|
||||||
|
CHECK(strcmp(entries[0].task_kind, "test.persisted") == 0
|
||||||
|
&& entries[0].task_kind_version == 1);
|
||||||
|
Lardon3DTask *restored = NULL;
|
||||||
|
CHECK(lardon3d_task_kind_registry_restore(
|
||||||
|
®istry, entries[0].task_kind, entries[0].task_kind_version,
|
||||||
|
&entries[0].snapshot, NULL, &restored) == LARDON3D_TASK_KIND_OK);
|
||||||
|
Lardon3DTaskSnapshot restored_snapshot;
|
||||||
|
CHECK(restored && lardon3d_task_snapshot(restored, &restored_snapshot));
|
||||||
|
CHECK(restored_snapshot.id == 1 && restored_snapshot.state == TASK_PENDING
|
||||||
|
&& restored_snapshot.progress == 20
|
||||||
|
&& lardon3d_task_sequence_count(restored) == 0);
|
||||||
|
lardon3d_task_destroy(restored);
|
||||||
|
|
||||||
|
Lardon3DTask *unknown = lardon3d_task_create_typed(
|
||||||
|
"Unknown", &estimate, "unknown.work", 1, unused_callback, NULL, NULL);
|
||||||
|
Lardon3DTask *future_kind = lardon3d_task_create_typed(
|
||||||
|
"Future kind", &estimate, "test.persisted", 2,
|
||||||
|
unused_callback, NULL, NULL);
|
||||||
|
CHECK(unknown && future_kind && lardon3d_task_assign_id(unknown, 4)
|
||||||
|
&& lardon3d_task_assign_id(future_kind, 5));
|
||||||
|
CHECK(lardon3d_project_checkpoint_task(&state, unknown)
|
||||||
|
== LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
CHECK(lardon3d_project_checkpoint_task(&state, future_kind)
|
||||||
|
== LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||||
|
Lardon3DTask *legacy = lardon3d_task_create(
|
||||||
|
"Legacy", &estimate, unused_callback, NULL);
|
||||||
|
CHECK(legacy && lardon3d_task_assign_id(legacy, 6));
|
||||||
|
CHECK(lardon3d_project_checkpoint_task(&state, legacy)
|
||||||
|
== LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK);
|
||||||
|
Lardon3DTaskDurableSnapshot legacy_snapshot;
|
||||||
|
CHECK(lardon3d_task_durable_snapshot(legacy, &legacy_snapshot));
|
||||||
|
char legacy_path[512];
|
||||||
|
CHECK(snprintf(legacy_path, sizeof(legacy_path),
|
||||||
|
"%s/.lardon3d/checkpoints/6.chk", project_path) > 0);
|
||||||
|
CHECK(lardon3d_task_checkpoint_save(legacy_path, &legacy_snapshot)
|
||||||
|
== LARDON3D_TASK_CHECKPOINT_OK);
|
||||||
|
Lardon3DProjectDbCheckpoint legacy_checkpoint = {
|
||||||
|
.format_version = LARDON3D_TASK_CHECKPOINT_VERSION,
|
||||||
|
.durability = LARDON3D_DB_CHECKPOINT_DURABLE,
|
||||||
|
.updated_at = 500,
|
||||||
|
};
|
||||||
|
(void)snprintf(legacy_checkpoint.path, sizeof(legacy_checkpoint.path),
|
||||||
|
".lardon3d/checkpoints/6.chk");
|
||||||
|
CHECK(lardon3d_project_db_record_task(state.project_db, &legacy_snapshot,
|
||||||
|
NULL, 0, &legacy_checkpoint, 500) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
Lardon3DProjectRecoveryEntry negative_entries[4];
|
||||||
|
CHECK(lardon3d_project_list_recoverable(&state, ®istry, 0,
|
||||||
|
negative_entries, 4, &count) == LARDON3D_PROJECT_DB_OK && count == 4);
|
||||||
|
CHECK(negative_entries[1].task_id == 4
|
||||||
|
&& negative_entries[1].status == LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND);
|
||||||
|
CHECK(negative_entries[2].task_id == 5
|
||||||
|
&& negative_entries[2].status
|
||||||
|
== LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION);
|
||||||
|
CHECK(negative_entries[3].task_id == 6
|
||||||
|
&& negative_entries[3].status == LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED);
|
||||||
|
lardon3d_task_destroy(legacy);
|
||||||
|
lardon3d_task_destroy(future_kind);
|
||||||
|
lardon3d_task_destroy(unknown);
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
|
|
||||||
CHECK(write_ini(ini_path, "Projet Cycle", "00000000000000000000000000000000", 2));
|
CHECK(write_ini(ini_path, "Projet Cycle", "00000000000000000000000000000000", 2));
|
||||||
|
|
@ -196,7 +291,12 @@ run_test(void)
|
||||||
lardon3d_task_destroy(task);
|
lardon3d_task_destroy(task);
|
||||||
|
|
||||||
char terminal_checkpoint[512]; CHECK(snprintf(terminal_checkpoint, sizeof(terminal_checkpoint), "%s/.lardon3d/checkpoints/2.chk", project_path) > 0);
|
char terminal_checkpoint[512]; CHECK(snprintf(terminal_checkpoint, sizeof(terminal_checkpoint), "%s/.lardon3d/checkpoints/2.chk", project_path) > 0);
|
||||||
|
char unknown_checkpoint[512], future_kind_checkpoint[512];
|
||||||
|
CHECK(snprintf(unknown_checkpoint, sizeof(unknown_checkpoint), "%s/.lardon3d/checkpoints/4.chk", project_path) > 0);
|
||||||
|
CHECK(snprintf(future_kind_checkpoint, sizeof(future_kind_checkpoint), "%s/.lardon3d/checkpoints/5.chk", project_path) > 0);
|
||||||
CHECK(unlink(checkpoint_path) == 0); CHECK(unlink(terminal_checkpoint) == 0);
|
CHECK(unlink(checkpoint_path) == 0); CHECK(unlink(terminal_checkpoint) == 0);
|
||||||
|
CHECK(unlink(unknown_checkpoint) == 0); CHECK(unlink(future_kind_checkpoint) == 0);
|
||||||
|
CHECK(unlink(legacy_path) == 0);
|
||||||
CHECK(unlink(database_path) == 0); CHECK(unlink(ini_path) == 0);
|
CHECK(unlink(database_path) == 0); CHECK(unlink(ini_path) == 0);
|
||||||
char path[512];
|
char path[512];
|
||||||
CHECK(snprintf(path, sizeof(path), "%s/.lardon3d/checkpoints", project_path) > 0 && rmdir(path) == 0);
|
CHECK(snprintf(path, sizeof(path), "%s/.lardon3d/checkpoints", project_path) > 0 && rmdir(path) == 0);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,31 @@ create_future_database(const char *path)
|
||||||
sqlite3 *connection = NULL;
|
sqlite3 *connection = NULL;
|
||||||
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
"INSERT INTO metadata VALUES('schema_version',2);", NULL, NULL, NULL) == SQLITE_OK;
|
"INSERT INTO metadata VALUES('schema_version',3);", NULL, NULL, NULL) == SQLITE_OK;
|
||||||
|
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
create_v1_database(const char *path)
|
||||||
|
{
|
||||||
|
static const char sql[] =
|
||||||
|
"PRAGMA foreign_keys=ON;"
|
||||||
|
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
|
"INSERT INTO metadata VALUES('schema_version',1);"
|
||||||
|
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||||
|
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),recovery_state INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),progress INTEGER NOT NULL CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL CHECK(sequence_count>=0),started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL CHECK(started_nsec BETWEEN 0 AND 999999999),finished_sec INTEGER NOT NULL,finished_nsec INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND 999999999),updated_at INTEGER NOT NULL);"
|
||||||
|
"CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);"
|
||||||
|
"CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,path TEXT NOT NULL,format_version INTEGER NOT NULL CHECK(format_version>0),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),updated_at INTEGER NOT NULL);"
|
||||||
|
"CREATE TABLE artifacts(artifact_id TEXT PRIMARY KEY,kind TEXT NOT NULL,path TEXT NOT NULL,state INTEGER NOT NULL CHECK(state BETWEEN 0 AND 1),size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||||
|
"CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);"
|
||||||
|
"CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);"
|
||||||
|
"INSERT INTO project VALUES(1,'legacy-project','Legacy',1,1);"
|
||||||
|
"INSERT INTO tasks VALUES(9,'Legacy task',1,0,12,3,1,0,0,0,2);"
|
||||||
|
"INSERT INTO checkpoints VALUES(9,'legacy.chk',1,0,2);"
|
||||||
|
"INSERT INTO artifacts VALUES('legacy-artifact','legacy','legacy.bin',0,0,9,2,2);";
|
||||||
|
sqlite3 *connection = NULL;
|
||||||
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
|
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,15 +109,18 @@ run_test(void)
|
||||||
char directory[] = "/tmp/lardon3d-project-db-XXXXXX";
|
char directory[] = "/tmp/lardon3d-project-db-XXXXXX";
|
||||||
CHECK(mkdtemp(directory));
|
CHECK(mkdtemp(directory));
|
||||||
char database_path[512], artifact_path[512], future_path[512], corrupt_path[512];
|
char database_path[512], artifact_path[512], future_path[512], corrupt_path[512];
|
||||||
|
char legacy_path[512], failed_migration_path[512];
|
||||||
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
||||||
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
||||||
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
||||||
CHECK(snprintf(corrupt_path, sizeof(corrupt_path), "%s/corrupt.db", directory) > 0);
|
CHECK(snprintf(corrupt_path, sizeof(corrupt_path), "%s/corrupt.db", directory) > 0);
|
||||||
|
CHECK(snprintf(legacy_path, sizeof(legacy_path), "%s/legacy.db", directory) > 0);
|
||||||
|
CHECK(snprintf(failed_migration_path, sizeof(failed_migration_path), "%s/failed-migration.db", directory) > 0);
|
||||||
|
|
||||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
Lardon3DProjectDb *database = NULL;
|
Lardon3DProjectDb *database = NULL;
|
||||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(database && lardon3d_project_db_schema_version(database) == 1);
|
CHECK(database && lardon3d_project_db_schema_version(database) == 2);
|
||||||
|
|
||||||
Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100};
|
Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100};
|
||||||
(void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001");
|
(void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001");
|
||||||
|
|
@ -115,22 +142,30 @@ run_test(void)
|
||||||
Lardon3DTaskDurableSnapshot running = task_snapshot(1, TASK_RUNNING);
|
Lardon3DTaskDurableSnapshot running = task_snapshot(1, TASK_RUNNING);
|
||||||
Lardon3DProjectDbCheckpoint checkpoint = {.format_version = 1, .durability = LARDON3D_DB_CHECKPOINT_DURABLE, .updated_at = 200};
|
Lardon3DProjectDbCheckpoint checkpoint = {.format_version = 1, .durability = LARDON3D_DB_CHECKPOINT_DURABLE, .updated_at = 200};
|
||||||
(void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s/checkpoints/task-1.chk", directory);
|
(void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s/checkpoints/task-1.chk", directory);
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 200) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 200) == LARDON3D_PROJECT_DB_OK);
|
||||||
running.progress = 30; running.sequence_count = 3;
|
running.progress = 30; running.sequence_count = 3;
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
||||||
Lardon3DProjectDbTask task;
|
Lardon3DProjectDbTask task;
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3);
|
CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3);
|
||||||
|
CHECK(task.has_task_kind && strcmp(task.task_kind, "test.work") == 0
|
||||||
|
&& task.task_kind_version == 1);
|
||||||
CHECK(task.has_checkpoint && strcmp(task.checkpoint.path, checkpoint.path) == 0);
|
CHECK(task.has_checkpoint && strcmp(task.checkpoint.path, checkpoint.path) == 0);
|
||||||
checkpoint.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
|
checkpoint.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
CHECK(task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
||||||
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.other", 1,
|
||||||
|
&checkpoint, 202) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||||
|
CHECK(lardon3d_project_db_record_task(database, &running, "Test.invalid", 1,
|
||||||
|
&checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 0,
|
||||||
|
&checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
|
|
||||||
Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED);
|
Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED);
|
||||||
CHECK(lardon3d_project_db_record_task(database, &completed, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &completed, "test.work", 1, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
||||||
Lardon3DTaskDurableSnapshot no_checkpoint = task_snapshot(4, TASK_PENDING);
|
Lardon3DTaskDurableSnapshot no_checkpoint = task_snapshot(4, TASK_PENDING);
|
||||||
CHECK(lardon3d_project_db_record_task(database, &no_checkpoint, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &no_checkpoint, NULL, 0, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
||||||
Lardon3DProjectDbTask page[1]; size_t count = 0;
|
Lardon3DProjectDbTask page[1]; size_t count = 0;
|
||||||
CHECK(lardon3d_project_db_list_recoverable(database, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_list_recoverable(database, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(count == 1 && page[0].task_id == 1);
|
CHECK(count == 1 && page[0].task_id == 1);
|
||||||
|
|
@ -139,7 +174,7 @@ run_test(void)
|
||||||
|
|
||||||
Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING);
|
Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING);
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT", "1", 1) == 0);
|
||||||
CHECK(lardon3d_project_db_record_task(database, &rollback_task, &checkpoint, 203) == LARDON3D_PROJECT_DB_IO_ERROR);
|
CHECK(lardon3d_project_db_record_task(database, &rollback_task, "test.work", 1, &checkpoint, 203) == LARDON3D_PROJECT_DB_IO_ERROR);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT") == 0);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 3, &task) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
CHECK(lardon3d_project_db_load_task(database, 3, &task) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||||
CHECK(lardon3d_project_db_test_orphan_checkpoint(database) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
CHECK(lardon3d_project_db_test_orphan_checkpoint(database) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||||
|
|
@ -175,7 +210,7 @@ run_test(void)
|
||||||
char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); too_long[sizeof(too_long) - 1] = '\0';
|
char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); too_long[sizeof(too_long) - 1] = '\0';
|
||||||
CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
lardon3d_project_db_close(contexts[0].database); database = NULL;
|
lardon3d_project_db_close(contexts[0].database); database = NULL;
|
||||||
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 1));
|
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 2));
|
||||||
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
||||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
|
@ -188,7 +223,34 @@ run_test(void)
|
||||||
CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0);
|
CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0);
|
||||||
CHECK(lardon3d_project_db_open(corrupt_path, &database, error) == LARDON3D_PROJECT_DB_CORRUPT);
|
CHECK(lardon3d_project_db_open(corrupt_path, &database, error) == LARDON3D_PROJECT_DB_CORRUPT);
|
||||||
|
|
||||||
|
CHECK(create_v1_database(legacy_path));
|
||||||
|
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(lardon3d_project_db_schema_version(database) == 2);
|
||||||
|
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
||||||
|
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(!task.has_task_kind && task.has_checkpoint
|
||||||
|
&& strcmp(task.checkpoint.path, "legacy.chk") == 0);
|
||||||
|
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
|
||||||
|
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
lardon3d_project_db_close(database); database = NULL;
|
||||||
|
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 2));
|
||||||
|
|
||||||
|
CHECK(create_v1_database(failed_migration_path));
|
||||||
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
||||||
|
CHECK(lardon3d_project_db_open(failed_migration_path, &database, error)
|
||||||
|
== LARDON3D_PROJECT_DB_IO_ERROR);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2") == 0);
|
||||||
|
CHECK(query_integer(failed_migration_path,
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version'", 1));
|
||||||
|
CHECK(lardon3d_project_db_open(failed_migration_path, &database, error)
|
||||||
|
== LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& !task.has_task_kind);
|
||||||
|
lardon3d_project_db_close(database); database = NULL;
|
||||||
|
|
||||||
CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); CHECK(unlink(corrupt_path) == 0);
|
CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); CHECK(unlink(corrupt_path) == 0);
|
||||||
|
CHECK(unlink(legacy_path) == 0); CHECK(unlink(failed_migration_path) == 0);
|
||||||
CHECK(rmdir(directory) == 0);
|
CHECK(rmdir(directory) == 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
172
tests/test_task_kind_registry.c
Normal file
172
tests/test_task_kind_registry.c
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <lardon3d/task_kind_registry.h>
|
||||||
|
|
||||||
|
#define CHECK(condition) do { if (!(condition)) { \
|
||||||
|
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
|
||||||
|
} } while (0)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int *destroyed;
|
||||||
|
uint64_t expected_id;
|
||||||
|
} TestUserdata;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int *destroyed;
|
||||||
|
bool fail;
|
||||||
|
} ReconstructContext;
|
||||||
|
|
||||||
|
static bool
|
||||||
|
test_callback(Lardon3DTask *task, void *userdata)
|
||||||
|
{
|
||||||
|
TestUserdata *data = userdata;
|
||||||
|
return data && lardon3d_task_id(task) == data->expected_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
destroy_userdata(void *userdata)
|
||||||
|
{
|
||||||
|
TestUserdata *data = userdata;
|
||||||
|
if (data) {
|
||||||
|
++*data->destroyed;
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
reconstruct(
|
||||||
|
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
|
void *context,
|
||||||
|
Lardon3DTaskKindBinding *binding
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ReconstructContext *settings = context;
|
||||||
|
TestUserdata *data = malloc(sizeof(*data));
|
||||||
|
if (!data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*data = (TestUserdata) {
|
||||||
|
.destroyed = settings->destroyed,
|
||||||
|
.expected_id = snapshot->id,
|
||||||
|
};
|
||||||
|
binding->callback = test_callback;
|
||||||
|
binding->userdata = data;
|
||||||
|
binding->userdata_destroy = destroy_userdata;
|
||||||
|
return !settings->fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Lardon3DTaskDurableSnapshot
|
||||||
|
snapshot(void)
|
||||||
|
{
|
||||||
|
Lardon3DTaskDurableSnapshot result = {
|
||||||
|
.id = 42,
|
||||||
|
.estimate = {
|
||||||
|
.minimum_batch_size = 1,
|
||||||
|
.maximum_batch_size = 1,
|
||||||
|
.desired_cpu_threads = 1,
|
||||||
|
.task_class = LARDON3D_RESOURCE_TASK_GENERAL,
|
||||||
|
},
|
||||||
|
.progress = 23,
|
||||||
|
.saved_state = TASK_RUNNING,
|
||||||
|
.recovery_state = TASK_PENDING,
|
||||||
|
.sequence_count = 4,
|
||||||
|
};
|
||||||
|
(void)snprintf(result.name, sizeof(result.name), "Registry test");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_test(void)
|
||||||
|
{
|
||||||
|
static const Lardon3DTaskKindDescriptor descriptors[] = {
|
||||||
|
{.kind = "test.recovery", .kind_version = 1, .reconstruct = reconstruct},
|
||||||
|
{.kind = "test.other", .kind_version = 1, .reconstruct = reconstruct},
|
||||||
|
};
|
||||||
|
Lardon3DTaskKindRegistry registry;
|
||||||
|
CHECK(lardon3d_task_kind_registry_init(®istry, descriptors, 2));
|
||||||
|
CHECK(lardon3d_task_kind_is_valid("test.recovery-1"));
|
||||||
|
CHECK(!lardon3d_task_kind_is_valid(""));
|
||||||
|
char too_long[LARDON3D_TASK_KIND_CAPACITY + 1];
|
||||||
|
memset(too_long, 'a', sizeof(too_long));
|
||||||
|
too_long[sizeof(too_long) - 1] = '\0';
|
||||||
|
CHECK(!lardon3d_task_kind_is_valid(too_long));
|
||||||
|
CHECK(!lardon3d_task_kind_is_valid("Test.recovery"));
|
||||||
|
CHECK(!lardon3d_task_kind_is_valid("test/recovery"));
|
||||||
|
CHECK(!lardon3d_task_kind_is_valid(".test"));
|
||||||
|
|
||||||
|
const Lardon3DTaskKindDescriptor *found = NULL;
|
||||||
|
CHECK(lardon3d_task_kind_registry_lookup(®istry, "test.recovery", 1,
|
||||||
|
&found) == LARDON3D_TASK_KIND_OK && found == &descriptors[0]);
|
||||||
|
found = NULL;
|
||||||
|
CHECK(lardon3d_task_kind_registry_lookup(®istry, "test.recovery", 1,
|
||||||
|
&found) == LARDON3D_TASK_KIND_OK && found == &descriptors[0]);
|
||||||
|
CHECK(lardon3d_task_kind_registry_lookup(®istry, "unknown.kind", 1,
|
||||||
|
&found) == LARDON3D_TASK_KIND_UNKNOWN);
|
||||||
|
CHECK(lardon3d_task_kind_registry_lookup(®istry, "test.recovery", 3,
|
||||||
|
&found) == LARDON3D_TASK_KIND_UNSUPPORTED_VERSION);
|
||||||
|
|
||||||
|
Lardon3DTaskKindDescriptor duplicate[] = {descriptors[0], descriptors[0]};
|
||||||
|
CHECK(!lardon3d_task_kind_registry_init(®istry, duplicate, 2));
|
||||||
|
CHECK(lardon3d_task_kind_registry_init(®istry, descriptors, 2));
|
||||||
|
|
||||||
|
int destroyed = 0;
|
||||||
|
ReconstructContext context = {.destroyed = &destroyed};
|
||||||
|
Lardon3DTaskDurableSnapshot durable = snapshot();
|
||||||
|
Lardon3DTask *task = NULL;
|
||||||
|
CHECK(lardon3d_task_kind_registry_restore(®istry, "test.recovery", 1,
|
||||||
|
&durable, &context, &task) == LARDON3D_TASK_KIND_OK);
|
||||||
|
CHECK(task && lardon3d_task_id(task) == durable.id
|
||||||
|
&& lardon3d_task_sequence_count(task) == durable.sequence_count);
|
||||||
|
char kind[LARDON3D_TASK_KIND_CAPACITY]; uint32_t version = 0;
|
||||||
|
CHECK(lardon3d_task_kind(task, kind, &version));
|
||||||
|
CHECK(strcmp(kind, "test.recovery") == 0 && version == 1);
|
||||||
|
Lardon3DHardwareProfile profile = {
|
||||||
|
.logical_cpu_count = 2,
|
||||||
|
.page_size_bytes = 4096,
|
||||||
|
.memory_total_bytes = 1024 * 1024,
|
||||||
|
.cpu_architecture = "test",
|
||||||
|
};
|
||||||
|
Lardon3DResourcePolicy policy = {
|
||||||
|
.maximum_cpu_load_ratio = 1.0,
|
||||||
|
.maximum_io_pressure_avg10 = 100.0,
|
||||||
|
.io_slot_capacity = 1,
|
||||||
|
};
|
||||||
|
Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create(
|
||||||
|
&profile, &policy);
|
||||||
|
Lardon3DResourceSnapshot resources = {
|
||||||
|
.memory_available_bytes = profile.memory_total_bytes,
|
||||||
|
.cpu_load_1m = 0.0,
|
||||||
|
};
|
||||||
|
Lardon3DResourceDecision decision;
|
||||||
|
Lardon3DResourceReservation *reservation = NULL;
|
||||||
|
CHECK(governor && lardon3d_resource_governor_reserve(
|
||||||
|
governor, &resources, &durable.estimate, &decision, &reservation));
|
||||||
|
CHECK(lardon3d_task_start(task, governor, reservation));
|
||||||
|
Lardon3DTaskSnapshot runtime;
|
||||||
|
CHECK(lardon3d_task_snapshot(task, &runtime)
|
||||||
|
&& runtime.state == TASK_COMPLETED);
|
||||||
|
lardon3d_task_destroy(task);
|
||||||
|
lardon3d_resource_governor_destroy(governor);
|
||||||
|
CHECK(destroyed == 1);
|
||||||
|
|
||||||
|
context.fail = true;
|
||||||
|
CHECK(lardon3d_task_kind_registry_restore(®istry, "test.recovery", 1,
|
||||||
|
&durable, &context, &task) == LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED);
|
||||||
|
CHECK(!task && destroyed == 2);
|
||||||
|
context.fail = false;
|
||||||
|
durable.progress = 101;
|
||||||
|
CHECK(lardon3d_task_kind_registry_restore(®istry, "test.recovery", 1,
|
||||||
|
&durable, &context, &task) == LARDON3D_TASK_KIND_RESTORE_FAILED);
|
||||||
|
CHECK(!task && destroyed == 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue