diff --git a/README.md b/README.md index 2f8d410..9f5c7c9 100644 --- a/README.md +++ b/README.md @@ -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 - **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **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 - **Hardware Profile** : détection des capacités matérielles - **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) -- 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 - Pools de workers multiples (CPU/GPU/IO) - Publication live validée @@ -94,6 +95,7 @@ Acquisitions - [Vue d'ensemble](docs/architecture/overview.md) - [Runtime](docs/architecture/runtime.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) - [Resource Governor](docs/architecture/resource_governor.md) - [Intégration Scheduler ↔ Governor](docs/architecture/scheduler_resource_integration.md) diff --git a/docs/architecture/persistence.md b/docs/architecture/persistence.md index e50f6ea..95a3e2f 100644 --- a/docs/architecture/persistence.md +++ b/docs/architecture/persistence.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 `time_t`. -## Project Database v1 +## Project Database v2 SQLite contient l'état logique interrogable et les références aux fichiers ; 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 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 **IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication 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. +**IMPLEMENTED** — registry statique bornée et reconstruction explicite avec +ownership du userdata. + **IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au 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. -**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v2+ et +**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v3+ et reprise globale. diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index 4160c7e..66e95c6 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -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 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, contrats ou réservations. Le fichier par tâche est une fondation, pas une 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 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 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 résumé durable. Les IDs v1 sont compris entre 1 et `INT64_MAX`. - `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 -Une DB vide reçoit le schéma v1 dans une transaction `BEGIN IMMEDIATE`. Une DB -v1 est validée puis ouverte. Une version future est refusée et une DB contenant +Une DB vide reçoit directement le schéma v2 dans une transaction +`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 -interne de migration ne connaît que `0 → 1`; aucune migration fictive ou -destructive n'est présente. +interne de migration ne connaît que `0 → 2` et `1 → 2`. -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 unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation 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 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 -**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. **IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente, publication de checkpoints par le projet et inventaire de reprise validé. -**NOT_YET_WIRED** — reconstruction des callbacks/userdata métier, resoumission -scheduler, autosave à toutes les transitions et réconciliation des checkpoints -orphelins, ScanSet et catalogue image persistants, Feature Store et Visual Index. +**IMPLEMENTED** — kinds persistants, classification par registry et +reconstruction explicite testée hors scheduler. -**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. diff --git a/docs/architecture/runtime.md b/docs/architecture/runtime.md index ae34e5f..56e2473 100644 --- a/docs/architecture/runtime.md +++ b/docs/architecture/runtime.md @@ -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 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 conserver l'identifiant stable restauré au lieu d'en assigner un nouveau. diff --git a/docs/architecture/task_kind_registry.md b/docs/architecture/task_kind_registry.md new file mode 100644 index 0000000..ebd7d88 --- /dev/null +++ b/docs/architecture/task_kind_registry.md @@ -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. diff --git a/docs/architecture/task_system.md b/docs/architecture/task_system.md index c5b161b..d0cfb84 100644 --- a/docs/architecture/task_system.md +++ b/docs/architecture/task_system.md @@ -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_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_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 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 ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace 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 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 -`task_restore()` tant que le type métier ne sait pas reconstruire callback et -userdata. +`task_restore()` que via un descriptor connu ; aucun pointeur n'est persistant. ## Limites diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 697d826..ea80ebf 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -29,9 +29,10 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la ### Phase 3 : Persistance - ✅ 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 -- 📋 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 ### Phase 4 : Pipeline avancé diff --git a/include/lardon3d/project.h b/include/lardon3d/project.h index 9af5d3a..b904455 100644 --- a/include/lardon3d/project.h +++ b/include/lardon3d/project.h @@ -5,6 +5,7 @@ #include #include +#include typedef enum { LARDON3D_PROJECT_TASK_CHECKPOINT_OK = 0, @@ -22,12 +23,17 @@ typedef enum { LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT, LARDON3D_PROJECT_RECOVERY_INVALID_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; typedef struct { uint64_t task_id; char name[LARDON3D_TASK_NAME_CAPACITY]; + char task_kind[LARDON3D_TASK_KIND_CAPACITY]; + uint32_t task_kind_version; Lardon3DProjectRecoveryStatus status; Lardon3DProjectDbCheckpointDurability durability; Lardon3DTaskDurableSnapshot snapshot; @@ -50,6 +56,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task( ); Lardon3DProjectDbResult lardon3d_project_list_recoverable( Lardon3DAppState *state, + const Lardon3DTaskKindRegistry *registry, uint64_t after_task_id, Lardon3DProjectRecoveryEntry *entries, size_t capacity, diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index 9fe1bff..817c8a8 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -9,7 +9,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 1, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 2, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -57,6 +57,9 @@ typedef struct { typedef struct { uint64_t task_id; 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 recovery_state; unsigned int progress; @@ -103,6 +106,8 @@ Lardon3DProjectDbResult lardon3d_project_db_get_project( Lardon3DProjectDbResult 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 ); diff --git a/include/lardon3d/task.h b/include/lardon3d/task.h index dcc1ef0..04a8553 100644 --- a/include/lardon3d/task.h +++ b/include/lardon3d/task.h @@ -10,6 +10,7 @@ enum { LARDON3D_TASK_NAME_CAPACITY = 128, LARDON3D_TASK_MESSAGE_CAPACITY = 256, + LARDON3D_TASK_KIND_CAPACITY = 65, }; typedef enum { @@ -23,6 +24,7 @@ typedef enum { typedef struct Lardon3DTask Lardon3DTask; typedef bool (*Lardon3DTaskCallback)(Lardon3DTask *task, void *userdata); +typedef void (*Lardon3DTaskUserdataDestroy)(void *userdata); typedef struct { uint64_t id; @@ -62,6 +64,15 @@ Lardon3DTask *lardon3d_task_create( Lardon3DTaskCallback callback, 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); /* Exécute le callback dans le thread appelant. */ bool lardon3d_task_start( @@ -93,6 +104,22 @@ Lardon3DTask *lardon3d_task_restore( Lardon3DTaskCallback callback, 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); bool lardon3d_task_assign_id(Lardon3DTask *task, uint64_t id); bool lardon3d_task_resource_estimate( diff --git a/include/lardon3d/task_kind_registry.h b/include/lardon3d/task_kind_registry.h new file mode 100644 index 0000000..a74dede --- /dev/null +++ b/include/lardon3d/task_kind_registry.h @@ -0,0 +1,65 @@ +#ifndef LARDON3D_TASK_KIND_REGISTRY_H +#define LARDON3D_TASK_KIND_REGISTRY_H + +#include +#include + +#include + +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 diff --git a/meson.build b/meson.build index adae776..53b0e4f 100644 --- a/meson.build +++ b/meson.build @@ -38,6 +38,7 @@ executable( 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', + 'src/task_kind_registry.c', 'src/task_queue.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -133,6 +134,9 @@ project_db_test = executable( sources: [ 'tests/test_project_db.c', 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', ], c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], include_directories: include_directories('include'), @@ -150,6 +154,7 @@ project_test = executable( 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', + 'src/task_kind_registry.c', 'src/image_catalog.c', 'src/image_view.c', 'src/resource_governor.c', @@ -165,6 +170,21 @@ project_test = executable( 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( 'test-sequential-task', sources: [ diff --git a/src/project.c b/src/project.c index dbd04de..021f7a0 100644 --- a/src/project.c +++ b/src/project.c @@ -861,6 +861,11 @@ lardon3d_project_checkpoint_task( if (!lardon3d_task_durable_snapshot(task, &snapshot) || snapshot.id == 0) { 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 absolute[PATH_MAX]; if (!checkpoint_paths(state, snapshot.id, relative, absolute)) { @@ -892,6 +897,8 @@ lardon3d_project_checkpoint_task( Lardon3DProjectDbResult recorded = lardon3d_project_db_record_task( state->project_db, &snapshot, + task_kind, + task_kind_version, &checkpoint, now.tv_sec ); @@ -923,6 +930,7 @@ coherent_recovery( Lardon3DProjectDbResult lardon3d_project_list_recoverable( Lardon3DAppState *state, + const Lardon3DTaskKindRegistry *registry, uint64_t after_task_id, Lardon3DProjectRecoveryEntry *entries, size_t capacity, @@ -932,7 +940,7 @@ lardon3d_project_list_recoverable( if (count) { *count = 0; } - if (!state || !state->project_loaded || !state->project_db || !entries + if (!state || !state->project_loaded || !state->project_db || !registry || !entries || !count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) { return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; @@ -962,9 +970,28 @@ lardon3d_project_list_recoverable( entry->task_id = tasks[index].task_id; entry->durability = tasks[index].checkpoint.durability; (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 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) { entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT; } else { diff --git a/src/project_db.c b/src/project_db.c index eb44df5..c998ab8 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -17,17 +17,19 @@ struct Lardon3DProjectDb { 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);" - "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)," "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," + "task_kind TEXT,task_kind_version INTEGER," "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);" + "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 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)," @@ -122,10 +124,35 @@ migrate(Lardon3DProjectDb *database, unsigned int from_version) copy_error(database->error, "Version de schéma future non supportée."); return LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA; } - if (from_version == 1) return LARDON3D_PROJECT_DB_OK; - if (from_version != 0) return LARDON3D_PROJECT_DB_CORRUPT; + if (from_version == LARDON3D_PROJECT_DB_SCHEMA_VERSION) { + 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"); - 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) (void)execute(database, "ROLLBACK", "rollback migration"); return result; @@ -187,7 +214,7 @@ lardon3d_project_db_open(const char *path, Lardon3DProjectDb **output, char erro unsigned int version = 0; 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 && version == 1) { + if (result == LARDON3D_PROJECT_DB_OK) { const char *required[] = {"project", "tasks", "checkpoints", "artifacts"}; for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++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); 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 step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context) @@ -323,9 +350,15 @@ valid_durable_task(const Lardon3DTaskDurableSnapshot *snapshot, int64_t updated_ Lardon3DProjectDbResult 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) { - 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 const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT"); 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"); sqlite3_stmt *statement = NULL; 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)" - "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," + "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,?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," - "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) { (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); - (void)sqlite3_bind_int(statement, 5, (int)snapshot->progress); (void)sqlite3_bind_int(statement, 6, (int)snapshot->sequence_count); - (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, 9, snapshot->finished_at.tv_sec); (void)sqlite3_bind_int64(statement, 10, snapshot->finished_at.tv_nsec); - (void)sqlite3_bind_int64(statement, 11, updated_at); result = step_done(database, statement, "upsert task"); + if (typed) { + (void)sqlite3_bind_text(statement, 3, task_kind, -1, SQLITE_TRANSIENT); + (void)sqlite3_bind_int64(statement, 4, task_kind_version); + } else { + (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) { #ifdef LARDON3D_PROJECT_DB_TESTING @@ -377,34 +422,45 @@ read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) { memset(task, 0, sizeof(*task)); sqlite3_int64 id = sqlite3_column_int64(statement, 0); - int progress = sqlite3_column_int(statement, 4); - sqlite3_int64 sequence_count = sqlite3_column_int64(statement, 5); - sqlite3_int64 started_nsec = sqlite3_column_int64(statement, 7); - sqlite3_int64 finished_nsec = sqlite3_column_int64(statement, 9); + bool has_kind = sqlite3_column_type(statement, 2) != SQLITE_NULL; + bool has_kind_version = sqlite3_column_type(statement, 3) != SQLITE_NULL; + int progress = sqlite3_column_int(statement, 6); + 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; - task->task_id = (uint64_t)id; task->saved_state = (Lardon3DTaskState)sqlite3_column_int(statement, 2); - task->recovery_state = (Lardon3DTaskState)sqlite3_column_int(statement, 3); + if (has_kind != has_kind_version) return false; + 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 || 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->finished_at.tv_sec)) return false; + || !database_time(sqlite3_column_int64(statement, 8), &task->started_at.tv_sec) + || !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->started_at.tv_nsec = (long)started_nsec; task->finished_at.tv_nsec = (long)finished_nsec; task->updated_at = sqlite3_column_int64(statement, 10); - if (sqlite3_column_type(statement, 11) != SQLITE_NULL) { + 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, 13) != SQLITE_NULL) { task->has_checkpoint = true; - if (!copy_column(statement, 11, task->checkpoint.path, sizeof(task->checkpoint.path))) return false; - sqlite3_int64 format_version = sqlite3_column_int64(statement, 12); + if (!copy_column(statement, 13, task->checkpoint.path, sizeof(task->checkpoint.path))) return false; + sqlite3_int64 format_version = sqlite3_column_int64(statement, 14); if (format_version <= 0 || format_version > UINT32_MAX) return false; task->checkpoint.format_version = (uint32_t)format_version; - task->checkpoint.durability = (Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 13); - task->checkpoint.updated_at = sqlite3_column_int64(statement, 14); + task->checkpoint.durability = (Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 15); + task->checkpoint.updated_at = sqlite3_column_int64(statement, 16); if (task->checkpoint.durability < LARDON3D_DB_CHECKPOINT_DURABLE || 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; } -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 "; Lardon3DProjectDbResult diff --git a/src/task.c b/src/task.c index 8cc5bbe..5e5dec4 100644 --- a/src/task.c +++ b/src/task.c @@ -18,6 +18,9 @@ struct Lardon3DTask { struct timespec finished_at; Lardon3DTaskCallback callback; void *userdata; + Lardon3DTaskUserdataDestroy userdata_destroy; + char task_kind[LARDON3D_TASK_KIND_CAPACITY]; + uint32_t task_kind_version; Lardon3DResourceEstimate estimate; Lardon3DTaskExecutionContract contract; bool has_contract; @@ -42,6 +45,28 @@ valid_state(Lardon3DTaskState state) 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 recovery_state(Lardon3DTaskState state) { @@ -81,7 +106,27 @@ lardon3d_task_create( 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; } Lardon3DTask *task = calloc(1, sizeof(*task)); @@ -102,6 +147,11 @@ lardon3d_task_create( task->state = TASK_PENDING; task->callback = callback; 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; copy_text(task->message, sizeof(task->message), "En attente."); return task; @@ -117,6 +167,9 @@ lardon3d_task_destroy(Lardon3DTask *task) (void)lardon3d_task_join(task); (void)pthread_cond_destroy(&task->condition); (void)pthread_mutex_destroy(&task->mutex); + if (task->userdata_destroy) { + task->userdata_destroy(task->userdata); + } free(task); } @@ -605,6 +658,21 @@ lardon3d_task_restore( Lardon3DTaskCallback callback, 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 || memchr(snapshot->name, '\0', sizeof(snapshot->name)) == NULL @@ -625,11 +693,14 @@ lardon3d_task_restore( && snapshot->estimate.desired_gpu_slots == 0)) { return NULL; } - Lardon3DTask *task = lardon3d_task_create( + Lardon3DTask *task = lardon3d_task_create_typed( snapshot->name, &snapshot->estimate, + task_kind, + task_kind_version, callback, - userdata + userdata, + userdata_destroy ); if (!task) { return NULL; @@ -646,6 +717,30 @@ lardon3d_task_restore( 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 lardon3d_task_id(const Lardon3DTask *task) { diff --git a/src/task_kind_registry.c b/src/task_kind_registry.c new file mode 100644 index 0000000..3c3cae6 --- /dev/null +++ b/src/task_kind_registry.c @@ -0,0 +1,116 @@ +#include +#include +#include + +#include + +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; +} diff --git a/tests/test_project.c b/tests/test_project.c index 5f8035d..3c04cd1 100644 --- a/tests/test_project.c +++ b/tests/test_project.c @@ -23,6 +23,25 @@ unused_callback(Lardon3DTask *task, void *userdata) 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 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); Lardon3DAppState 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(state.project_loaded && state.project_db && strlen(state.project_stable_id) == 32); @@ -97,7 +118,9 @@ run_test(void) .maximum_batch_size = 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(lardon3d_task_set_progress(task, 10, "frontière 10")); 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(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(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(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(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); - 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(setenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE", "1", 1) == 0); 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_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(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(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(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK); int descriptor = open(checkpoint_path, O_WRONLY | O_TRUNC); CHECK(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(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK); descriptor = open(checkpoint_path, O_RDWR); CHECK(descriptor >= 0); unsigned char future_version[4] = {2, 0, 0, 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(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}}; 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(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(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_project_close(&state); CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT); 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(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); CHECK(write_ini(ini_path, "Projet Cycle", "00000000000000000000000000000000", 2)); @@ -196,7 +291,12 @@ run_test(void) lardon3d_task_destroy(task); 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(unknown_checkpoint) == 0); CHECK(unlink(future_kind_checkpoint) == 0); + CHECK(unlink(legacy_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(ini_path) == 0); char path[512]; CHECK(snprintf(path, sizeof(path), "%s/.lardon3d/checkpoints", project_path) > 0 && rmdir(path) == 0); diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 5805a9b..7184569 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -57,7 +57,31 @@ create_future_database(const char *path) sqlite3 *connection = NULL; 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);" - "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; } @@ -85,15 +109,18 @@ run_test(void) char directory[] = "/tmp/lardon3d-project-db-XXXXXX"; CHECK(mkdtemp(directory)); 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(artifact_path, sizeof(artifact_path), "%s/artifact.bin", 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(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]; Lardon3DProjectDb *database = NULL; 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}; (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); 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); - 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; - 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; 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.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); 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(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); - 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); - 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; CHECK(lardon3d_project_db_list_recoverable(database, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK); CHECK(count == 1 && page[0].task_id == 1); @@ -139,7 +174,7 @@ run_test(void) Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING); 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(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); @@ -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'; CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); 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(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); @@ -188,7 +223,34 @@ run_test(void) CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0); 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(legacy_path) == 0); CHECK(unlink(failed_migration_path) == 0); CHECK(rmdir(directory) == 0); return true; } diff --git a/tests/test_task_kind_registry.c b/tests/test_task_kind_registry.c new file mode 100644 index 0000000..4d48560 --- /dev/null +++ b/tests/test_task_kind_registry.c @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include + +#include + +#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; +}