feat(task): add durable task kind registry

This commit is contained in:
fy59 2026-08-08 11:11:21 +02:00
parent efa556a824
commit 2c122e495f
19 changed files with 925 additions and 86 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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.

View 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.

View file

@ -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

View file

@ -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é

View file

@ -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,

View file

@ -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
); );

View file

@ -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(

View 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

View file

@ -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: [

View file

@ -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 {

View file

@ -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

View file

@ -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
View 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 =
&registry->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;
}

View file

@ -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(&registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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, &registry, 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(
&registry, 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, &registry, 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);

View file

@ -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;
} }

View 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(&registry, 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(&registry, "test.recovery", 1,
&found) == LARDON3D_TASK_KIND_OK && found == &descriptors[0]);
found = NULL;
CHECK(lardon3d_task_kind_registry_lookup(&registry, "test.recovery", 1,
&found) == LARDON3D_TASK_KIND_OK && found == &descriptors[0]);
CHECK(lardon3d_task_kind_registry_lookup(&registry, "unknown.kind", 1,
&found) == LARDON3D_TASK_KIND_UNKNOWN);
CHECK(lardon3d_task_kind_registry_lookup(&registry, "test.recovery", 3,
&found) == LARDON3D_TASK_KIND_UNSUPPORTED_VERSION);
Lardon3DTaskKindDescriptor duplicate[] = {descriptors[0], descriptors[0]};
CHECK(!lardon3d_task_kind_registry_init(&registry, duplicate, 2));
CHECK(lardon3d_task_kind_registry_init(&registry, descriptors, 2));
int destroyed = 0;
ReconstructContext context = {.destroyed = &destroyed};
Lardon3DTaskDurableSnapshot durable = snapshot();
Lardon3DTask *task = NULL;
CHECK(lardon3d_task_kind_registry_restore(&registry, "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(&registry, "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(&registry, "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;
}