feat(project): resume recoverable tasks on project open

This commit is contained in:
fy59 2026-08-08 11:57:28 +02:00
parent 02c2ecb22c
commit 36d083f727
20 changed files with 542 additions and 66 deletions

View file

@ -65,8 +65,8 @@ disponible. Ne jamais annoncer une vérification non exécutée.
- Gouverneur thread-safe avec estimations, budgets et réservations opaques.
- Scheduler relié au gouverneur ; réservation obligatoire avant callback.
- Aucun DAG, aucune priorité, aucun pool de workers.
- L'import asynchrone utilise encore son worker dédié et n'est pas migré vers
le scheduler générique.
- L'import asynchrone `import.images` utilise le scheduler générique, ses lots
adaptatifs et ses checkpoints persistants.
- Le viewer Vulkan n'est pas commencé.
## Prochains tickets recommandés

View file

@ -28,6 +28,7 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v3** : identité, tâches typées/checkpoints, paramètres d'import et inventaire d'artefacts SQLite
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
- **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)
- Resoumission contrôlée des tâches récupérables à l'ouverture
- Graphe de dépendances pour ordonner les futures reprises interdépendantes
- DAG de dépendances
- Pools de workers multiples (CPU/GPU/IO)
- Publication live validée

View file

@ -172,8 +172,17 @@ checkpoint de fin de lot. Si ce checkpoint ou sa transaction DB échoue, le
manifeste demeure la frontière idempotente plus récente et un checkpoint
orphelin peut subsister selon le protocole filesystem puis SQLite.
**NOT_YET_WIRED** — autosave complet, resoumission scheduler et réconciliation
des fichiers orphelins.
**IMPLEMENTED** — reprise automatique sélective à l'ouverture : pagination
bornée, validation checkpoint/kind, reconstruction production et enqueue sans
claim persistant supplémentaire.
Les records sont parcourus par task ID croissant. Un checkpoint
`PUBLISHED_NOT_DURABLE` présent, valide et cohérent peut être repris ; le résumé
conserve cet avertissement jusqu'au prochain checkpoint durable. Une tâche
terminale n'appartient pas à la requête de reprise.
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
retry piloté par l'utilisateur pour les sources indisponibles.
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v4+ et
reprise globale.
reprise avec dépendances.

View file

@ -274,6 +274,18 @@ 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.
À l'ouverture, le projet lit des pages de 8 dans l'ordre croissant des task
IDs. Chaque record est copié hors mutex DB avant reconstruction et enqueue. Une
fenêtre pleine interrompt le scan sans modifier les tâches restantes. Le résumé
borné expose `inspected`, `resumed`, `skipped`, `failed`, le nombre de
checkpoints `PUBLISHED_NOT_DURABLE` repris et la saturation éventuelle.
Les erreurs d'ouverture/migration, de schéma ou d'identité restent fatales.
Les erreurs propres à une tâche — legacy, kind inconnu/futur, checkpoint
absent/invalide/futur, source indisponible ou reconstruction — sont non fatales.
Un `BUSY` après le timeout SQLite arrête le scan sans boucle et laisse le projet
ouvert.
## Statut
**IMPLEMENTED** — SQLite système, schéma v3 et migrations v1→v2→v3, identité
@ -289,8 +301,11 @@ reconstruction explicite testée hors scheduler.
**IMPLEMENTED** — allocation transactionnelle de task IDs, paramètres
immuables de `import.images` et reconstruction production explicite.
**NOT_YET_WIRED** — resoumission automatique, autosave
à toutes les transitions et réconciliation des checkpoints orphelins, ScanSet
**IMPLEMENTED** — reprise automatique sélective, pagination de 8, ordre par ID,
fenêtre de queue non bloquante et résumé consultable.
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
indisponibles et réconciliation des checkpoints orphelins, ScanSet
et catalogue image persistants, Feature Store et Visual Index.
**PLANNED** — migrations v4+, dépendances d'artefacts, graphe géométrique et

View file

@ -57,7 +57,7 @@
- Worker unique (pas de pools multiples)
- Pas de parallélisme inter-tâches
- Checkpoints isolés disponibles mais pas encore orchestrés au démarrage
- Reprise automatique limitée aux tâches indépendantes reconstructibles
## Reprise durable
@ -78,8 +78,17 @@ qu'à la terminaison de cette tentative.
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et
se reconstruit explicitement avec un userdata neuf lié au projet rouvert.
**NOT_YET_WIRED** — resoumission automatique à l'ouverture et autosave
générique des autres kinds.
**IMPLEMENTED** — `project_open()` inventorie par pages de 8, restaure puis
resoumet automatiquement les tâches production valides. Il retourne après
l'enqueue et n'attend jamais leur terminaison.
L'ordre d'initialisation production est : profil matériel, governor, queue et
worker, TUI, puis ouverture DB/projet et reprise synchrone. Une fermeture ne
peut commencer qu'après le retour de `project_open()`. Le worker peut consommer
pendant le scan ; chaque tâche exécutée est néanmoins réadmise normalement.
**NOT_YET_WIRED** — autosave générique des autres kinds et reprise ordonnée par
dépendances.
**PLANNED** — reprise globale du scheduler via la Project Database.
@ -99,7 +108,10 @@ mutex DB pour un kind connu ; elle ne soumet aucune tâche.
n'entre en collision avec aucune tâche connue. L'import production peut donc
être reconstruit puis soumis explicitement.
**NOT_YET_WIRED** — aucune resoumission automatique à `project_open()`.
**IMPLEMENTED** — la resoumission automatique utilise la registry production,
conserve le task ID et laisse le worker obtenir une nouvelle réservation.
Kinds inconnus, tâches legacy, checkpoints invalides et sources absentes ne
bloquent pas l'ouverture.
## Invariants

View file

@ -41,8 +41,10 @@ migration DB v1→v2, classification recovery et restauration explicite testée.
le chemin source borné depuis la table dédiée et reconstruit callback et
userdata sans `AppState *` ancien.
**NOT_YET_WIRED** — autosave complet, resoumission automatique et
réconciliation orpheline.
**IMPLEMENTED** — `project_open()` utilise la registry production immutable
pour restaurer hors mutex DB et transférer chaque tâche acceptée à la queue.
**NOT_YET_WIRED** — autosave complet et réconciliation orpheline.
**PLANNED** — kinds des tâches ScanSet, Image Catalog, Feature Store, Visual
Index et reconstruction lorsque ces traitements existeront réellement.

View file

@ -29,6 +29,8 @@ Le type public réel est l'opaque `Lardon3DTaskQueue`.
| `lardon3d_task_queue_destroy()` | Arrête, annule, attend puis libère la file |
| `lardon3d_task_queue_add()` | Ajoute avec backpressure bloquante |
| `lardon3d_task_queue_try_add()` | Ajoute sans bloquer si une place existe |
| `lardon3d_task_queue_try_add_ex()` | Distingue succès, saturation, arrêt, collision d'ID et erreur |
| `lardon3d_task_queue_pause()` / `resume()` | Contrôle une tâche par son ID stable |
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
| `lardon3d_task_queue_remove()` | Retire une tâche terminale |
| `lardon3d_task_queue_snapshot()` | Copie une vue bornée de la file |
@ -75,14 +77,20 @@ le blocage par la tête de file lorsqu'une tâche ne peut pas démarrer.
## Statut
**IMPLÉMENTÉ** — file FIFO avec worker unique, sélection adaptative, pause et
annulation coopératives.
**IMPLÉMENTÉ** — file FIFO avec worker unique, sélection adaptative, pause,
annulation coopérative et accueil des tâches restaurées avec ID préassigné.
La reprise projet utilise `try_add_ex()` et ne bloque jamais `project_open()`.
À saturation, elle arrête sa fenêtre : les tâches non transférées restent
`PENDING` en DB et seront réévaluées lors d'une ouverture ultérieure. Ce n'est
pas un second scheduler.
## Limites
- Worker unique : pas de parallélisme interne.
- Pas de DAG ni de dépendances inter-tâches.
- Pas de priorités (FIFO strict).
- La reprise depuis Project Database n'est pas encore branchée.
- La reprise ne possède pas encore de DAG ni de déclenchement différé lorsque
une place se libère pendant la session courante.
- Pas de pool de workers CPU/IO/GPU.
- La backpressure borne les producteurs à la capacité configurée.

View file

@ -83,7 +83,10 @@ par la tâche restaurée et exécuté après arrêt de son exécution.
de séquence et les checkpoints génériques ; une tâche restaurée conserve son ID
lors de sa soumission explicite à la file.
**NOT_YET_WIRED** — autosave générique et resoumission automatique.
**IMPLEMENTED** — resoumission automatique sélective des tâches production à
l'ouverture du projet.
**NOT_YET_WIRED** — autosave générique et dépendances entre tâches.
Le chemin de production de l'import ne possède plus de thread ni de drapeau
d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la
@ -98,6 +101,11 @@ avant l'appel hors mutex. `join()` attend la fin du callback ; le userdata reste
donc valide pendant celui-ci et son destructeur n'est appelé qu'ensuite par la
destruction de la tâche.
Une tâche reconstruite mais refusée avant transfert à la queue est abandonnée
localement : son userdata est détruit, sans callback terminal ni écriture
durable d'une fausse annulation. Une annulation explicitement demandée conserve
le contrat de notification terminale.
La Project Database v3 peut enregistrer transactionnellement un résumé
`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
@ -111,6 +119,6 @@ d'inventaire retourne des snapshots durables validés, mais ne peut pas appeler
## Limites
- Aucune priorité interne : l'ordre est uniquement FIFO.
- Pas encore de reprise globale au démarrage du projet.
- Pas encore de DAG pour ordonner des reprises interdépendantes.
- Aucune dépendance inter-tâches (pas de DAG).
- Pas encore de références d'artefacts métier validés.

View file

@ -33,7 +33,7 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
- ✅ Registry durable des types métier de tâches
- ✅ Premier type métier reconstructible (`import.images`)
- 📋 Resoumission automatique contrôlée des tâches récupérables
- ✅ Resoumission automatique contrôlée et bornée des tâches récupérables
- 📋 ScanSet et Image Catalog persistants
### Phase 4 : Pipeline avancé

View file

@ -2,6 +2,7 @@
#define LARDON3D_APP_STATE_H
#include <stdbool.h>
#include <stddef.h>
#include <limits.h>
#include <lardon3d/hardware_profile.h>
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
@ -34,6 +35,12 @@ typedef struct {
Lardon3DHardwareProfile hardware_profile;
Lardon3DResourceGovernor *resource_governor;
Lardon3DProjectDb *project_db;
size_t recovery_inspected;
size_t recovery_resumed;
size_t recovery_skipped;
size_t recovery_failed;
size_t recovery_published_not_durable;
bool recovery_queue_full;
} Lardon3DAppState;
void lardon3d_app_state_init(Lardon3DAppState *state);

View file

@ -39,6 +39,15 @@ typedef struct {
Lardon3DTaskDurableSnapshot snapshot;
} Lardon3DProjectRecoveryEntry;
typedef struct {
size_t inspected;
size_t resumed;
size_t skipped;
size_t failed;
size_t published_not_durable;
bool queue_full;
} Lardon3DProjectRecoverySummary;
bool lardon3d_project_create(
Lardon3DAppState *state,
const char *name
@ -68,5 +77,14 @@ Lardon3DProjectDbResult lardon3d_project_list_recoverable(
size_t capacity,
size_t *count
);
Lardon3DProjectDbResult lardon3d_project_resume_recoverable_tasks(
Lardon3DAppState *state,
const Lardon3DTaskKindRegistry *registry,
Lardon3DProjectRecoverySummary *summary
);
bool lardon3d_project_last_recovery_summary(
const Lardon3DAppState *state,
Lardon3DProjectRecoverySummary *summary
);
#endif

View file

@ -9,6 +9,14 @@
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
typedef enum {
LARDON3D_TASK_QUEUE_ADD_OK = 0,
LARDON3D_TASK_QUEUE_ADD_FULL,
LARDON3D_TASK_QUEUE_ADD_STOPPING,
LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID,
LARDON3D_TASK_QUEUE_ADD_ERROR
} Lardon3DTaskQueueAddResult;
typedef struct {
size_t running;
size_t pending;
@ -35,8 +43,15 @@ bool lardon3d_task_queue_try_add(
Lardon3DTask *task,
uint64_t *task_id
);
Lardon3DTaskQueueAddResult lardon3d_task_queue_try_add_ex(
Lardon3DTaskQueue *queue,
Lardon3DTask *task,
uint64_t *task_id
);
bool lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id);
bool lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id);
bool lardon3d_task_queue_pause(Lardon3DTaskQueue *queue, uint64_t task_id);
bool lardon3d_task_queue_resume(Lardon3DTaskQueue *queue, uint64_t task_id);
void lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue);
size_t lardon3d_task_queue_count(Lardon3DTaskQueue *queue);
bool lardon3d_task_queue_get(

View file

@ -172,6 +172,10 @@ project_test = executable(
'src/task.c',
'src/task_checkpoint.c',
'src/task_kind_registry.c',
'src/task_kinds.c',
'src/task_queue.c',
'src/import.c',
'src/import_task.c',
'src/image_catalog.c',
'src/image_view.c',
'src/resource_governor.c',

View file

@ -14,8 +14,10 @@
#include <lardon3d/project.h>
#include <lardon3d/image_catalog.h>
#include <lardon3d/image_view.h>
#include <lardon3d/import_task.h>
#include <lardon3d/project_db.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
enum {
MAX_CREATED_DIRECTORIES = 16,
@ -44,6 +46,19 @@ set_status(Lardon3DAppState *state, const char *message)
);
}
static void
store_recovery_summary(Lardon3DAppState *state,
const Lardon3DProjectRecoverySummary *summary)
{
state->recovery_inspected = summary ? summary->inspected : 0;
state->recovery_resumed = summary ? summary->resumed : 0;
state->recovery_skipped = summary ? summary->skipped : 0;
state->recovery_failed = summary ? summary->failed : 0;
state->recovery_published_not_durable = summary
? summary->published_not_durable : 0;
state->recovery_queue_full = summary && summary->queue_full;
}
static void
set_database_status(
Lardon3DAppState *state,
@ -475,6 +490,7 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
clear_catalog(state);
state->project_loaded = true;
store_recovery_summary(state, NULL);
state->project_db = database;
(void)copy_path(
state->project_name,
@ -798,12 +814,23 @@ lardon3d_project_open(
sizeof(state->project_path),
project_path
);
store_recovery_summary(state, NULL);
if (state->task_queue && state->resource_governor) {
Lardon3DProjectRecoverySummary summary;
(void)lardon3d_project_resume_recoverable_tasks(state,
lardon3d_task_kind_registry_production(), &summary);
(void)snprintf(state->status_message, sizeof(state->status_message),
"Projet ouvert — %zu tâche(s) reprise(s), %zu ignorée(s), %zu en échec%s.",
summary.resumed, summary.skipped, summary.failed,
summary.queue_full ? ", fenêtre de reprise saturée" : "");
} else {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Projet ouvert : %s",
state->project_name
);
}
return true;
}
@ -826,6 +853,7 @@ lardon3d_project_close(Lardon3DAppState *state)
state->project_name[0] = '\0';
state->project_path[0] = '\0';
state->project_stable_id[0] = '\0';
store_recovery_summary(state, NULL);
set_status(state, "Projet fermé.");
}
@ -1051,3 +1079,109 @@ lardon3d_project_list_recoverable(
}
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult
lardon3d_project_resume_recoverable_tasks(
Lardon3DAppState *state,
const Lardon3DTaskKindRegistry *registry,
Lardon3DProjectRecoverySummary *summary
)
{
if (summary) memset(summary, 0, sizeof(*summary));
if (!state || !state->project_loaded || !state->project_db
|| !state->task_queue || !state->resource_governor || !registry
|| !summary) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
enum { RECOVERY_PAGE_SIZE = 8 };
uint64_t cursor = 0;
Lardon3DProjectDbResult result = LARDON3D_PROJECT_DB_OK;
bool stop = false;
do {
Lardon3DProjectRecoveryEntry entries[RECOVERY_PAGE_SIZE];
size_t count = 0;
result = lardon3d_project_list_recoverable(state, registry, cursor,
entries, RECOVERY_PAGE_SIZE, &count);
if (result != LARDON3D_PROJECT_DB_OK) {
++summary->failed;
break;
}
for (size_t index = 0; index < count; ++index) {
Lardon3DProjectRecoveryEntry *entry = &entries[index];
cursor = entry->task_id;
++summary->inspected;
bool recoverable = entry->status == LARDON3D_PROJECT_RECOVERABLE
|| entry->status
== LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE;
if (!recoverable || entry->snapshot.recovery_state != TASK_PENDING) {
++summary->skipped;
continue;
}
if (entry->status
== LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE) {
++summary->published_not_durable;
}
Lardon3DTaskSnapshot existing;
if (lardon3d_task_queue_get(state->task_queue, entry->task_id,
&existing)) {
++summary->skipped;
continue;
}
Lardon3DImageImportReconstructionContext context = {
.project_path = state->project_path,
.project_db = state->project_db,
.resource_governor = state->resource_governor,
};
Lardon3DTask *task = NULL;
Lardon3DTaskKindResult restored =
lardon3d_task_kind_registry_restore(registry,
entry->task_kind, entry->task_kind_version,
&entry->snapshot, &context, &task);
if (restored != LARDON3D_TASK_KIND_OK || !task) {
++summary->failed;
continue;
}
Lardon3DTaskQueueAddResult added =
lardon3d_task_queue_try_add_ex(state->task_queue, task, NULL);
if (added == LARDON3D_TASK_QUEUE_ADD_OK) {
++summary->resumed;
continue;
}
lardon3d_task_destroy(task);
if (added == LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID) {
++summary->skipped;
} else if (added == LARDON3D_TASK_QUEUE_ADD_FULL) {
++summary->skipped;
} else {
++summary->failed;
}
if (added == LARDON3D_TASK_QUEUE_ADD_FULL
|| added == LARDON3D_TASK_QUEUE_ADD_STOPPING) {
summary->queue_full = added == LARDON3D_TASK_QUEUE_ADD_FULL;
stop = true;
break;
}
}
if (stop || count < RECOVERY_PAGE_SIZE) break;
} while (cursor > 0);
store_recovery_summary(state, summary);
return result;
}
bool
lardon3d_project_last_recovery_summary(
const Lardon3DAppState *state,
Lardon3DProjectRecoverySummary *summary
)
{
if (!state || !summary) return false;
*summary = (Lardon3DProjectRecoverySummary) {
.inspected = state->recovery_inspected,
.resumed = state->recovery_resumed,
.skipped = state->recovery_skipped,
.failed = state->recovery_failed,
.published_not_durable = state->recovery_published_not_durable,
.queue_full = state->recovery_queue_full,
};
return true;
}

View file

@ -565,6 +565,12 @@ lardon3d_project_db_list_recoverable(Lardon3DProjectDb *database, uint64_t after
{
if (count) *count = 0;
if (!database || !tasks || !count || after_task_id > INT64_MAX || capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY");
if (forced_busy && strcmp(forced_busy, "1") == 0) {
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL; char sql[896];
(void)snprintf(sql, sizeof(sql), "%s WHERE t.recovery_state=?1 AND c.task_id IS NOT NULL AND t.task_id>?2 ORDER BY t.task_id LIMIT ?3", task_select);
Lardon3DProjectDbResult result = prepare(database, sql, &statement);

View file

@ -189,7 +189,21 @@ lardon3d_task_destroy(Lardon3DTask *task)
if (!task) {
return;
}
lardon3d_task_request_cancel(task);
(void)pthread_mutex_lock(&task->mutex);
if (!is_terminal(task->state)) {
if (task->executing) {
task->cancel_requested = true;
copy_text(task->message, sizeof(task->message),
"Annulation demandée.");
(void)pthread_cond_broadcast(&task->condition);
} else {
/* Une tâche locale jamais soumise peut être abandonnée sans
* publier une fausse annulation métier. */
finish_locked(task, TASK_CANCELLED, "Tâche abandonnée.");
task->finished_notified = true;
}
}
(void)pthread_mutex_unlock(&task->mutex);
(void)lardon3d_task_join(task);
(void)pthread_cond_destroy(&task->condition);
(void)pthread_mutex_destroy(&task->mutex);

View file

@ -228,6 +228,35 @@ lardon3d_task_queue_cancel(Lardon3DTaskQueue *queue, uint64_t task_id)
return node != NULL;
}
bool
lardon3d_task_queue_pause(Lardon3DTaskQueue *queue, uint64_t task_id)
{
if (!queue || task_id == 0) return false;
(void)pthread_mutex_lock(&queue->mutex);
TaskNode *node = queue->all_head;
while (node && lardon3d_task_id(node->task) != task_id) {
node = node->next_all;
}
bool paused = node && lardon3d_task_pause(node->task);
(void)pthread_mutex_unlock(&queue->mutex);
return paused;
}
bool
lardon3d_task_queue_resume(Lardon3DTaskQueue *queue, uint64_t task_id)
{
if (!queue || task_id == 0) return false;
(void)pthread_mutex_lock(&queue->mutex);
TaskNode *node = queue->all_head;
while (node && lardon3d_task_id(node->task) != task_id) {
node = node->next_all;
}
bool resumed = node && lardon3d_task_resume(node->task);
if (resumed) (void)pthread_cond_broadcast(&queue->not_empty);
(void)pthread_mutex_unlock(&queue->mutex);
return resumed;
}
void
lardon3d_task_queue_resources_changed(Lardon3DTaskQueue *queue)
{
@ -356,22 +385,50 @@ lardon3d_task_queue_try_add(
Lardon3DTask *task,
uint64_t *task_id
)
{
return lardon3d_task_queue_try_add_ex(queue, task, task_id)
== LARDON3D_TASK_QUEUE_ADD_OK;
}
Lardon3DTaskQueueAddResult
lardon3d_task_queue_try_add_ex(
Lardon3DTaskQueue *queue,
Lardon3DTask *task,
uint64_t *task_id
)
{
if (!queue || !task) {
return false;
return LARDON3D_TASK_QUEUE_ADD_ERROR;
}
TaskNode *node = calloc(1, sizeof(*node));
if (!node) {
return false;
return LARDON3D_TASK_QUEUE_ADD_ERROR;
}
(void)pthread_mutex_lock(&queue->mutex);
bool accepted = queue->pending_count < queue->capacity
&& enqueue_locked(queue, node, task, task_id);
Lardon3DTaskQueueAddResult result = LARDON3D_TASK_QUEUE_ADD_OK;
if (queue->stopping) {
result = LARDON3D_TASK_QUEUE_ADD_STOPPING;
} else if (queue->pending_count >= queue->capacity) {
result = LARDON3D_TASK_QUEUE_ADD_FULL;
} else {
uint64_t id = lardon3d_task_id(task);
for (TaskNode *existing = queue->all_head; existing;
existing = existing->next_all) {
if (id != 0 && lardon3d_task_id(existing->task) == id) {
result = LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID;
break;
}
}
if (result == LARDON3D_TASK_QUEUE_ADD_OK
&& !enqueue_locked(queue, node, task, task_id)) {
result = LARDON3D_TASK_QUEUE_ADD_ERROR;
}
}
(void)pthread_mutex_unlock(&queue->mutex);
if (!accepted) {
if (result != LARDON3D_TASK_QUEUE_ADD_OK) {
free(node);
}
return accepted;
return result;
}
bool

View file

@ -12,6 +12,7 @@
#include <lardon3d/image_catalog.h>
#include <lardon3d/import_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
#define CHECK(condition) do { if (!(condition)) { \
@ -55,7 +56,7 @@ remove_tree(const char *path)
}
static bool
setup_runtime(Lardon3DAppState *state)
setup_runtime(Lardon3DAppState *state, size_t queue_capacity)
{
state->hardware_profile = (Lardon3DHardwareProfile) {
.logical_cpu_count = 1024,
@ -71,7 +72,8 @@ setup_runtime(Lardon3DAppState *state)
state->resource_governor = lardon3d_resource_governor_create(
&state->hardware_profile, &policy);
state->task_queue = state->resource_governor
? lardon3d_task_queue_create(state->resource_governor, 4) : NULL;
? lardon3d_task_queue_create(state->resource_governor, queue_capacity)
: NULL;
return state->task_queue != NULL;
}
@ -101,7 +103,7 @@ run_test(void)
}
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
Lardon3DAppState state; lardon3d_app_state_init(&state);
CHECK(setup_runtime(&state));
CHECK(setup_runtime(&state, 4));
CHECK(lardon3d_project_create(&state, "Persistent Import"));
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
@ -113,42 +115,45 @@ run_test(void)
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
CHECK(runtime.progress > 0 && runtime.progress < 100);
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
char checkpoint_path[PATH_MAX];
CHECK(snprintf(checkpoint_path, sizeof(checkpoint_path),
"%s/.lardon3d/checkpoints/%llu.chk", state.project_path,
(unsigned long long)task_id) > 0);
Lardon3DTaskDurableSnapshot persisted_snapshot;
CHECK(lardon3d_task_checkpoint_load(checkpoint_path, &persisted_snapshot,
NULL) == LARDON3D_TASK_CHECKPOINT_OK);
Lardon3DProjectDbCheckpoint published_not_durable = {
.format_version = LARDON3D_TASK_CHECKPOINT_VERSION,
.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE,
.updated_at = 1,
};
CHECK(snprintf(published_not_durable.path,
sizeof(published_not_durable.path),
".lardon3d/checkpoints/%llu.chk",
(unsigned long long)task_id) > 0);
CHECK(lardon3d_project_db_record_image_import_task(state.project_db,
&persisted_snapshot, LARDON3D_IMAGE_IMPORT_TASK_KIND,
LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION, &published_not_durable,
source, 1) == LARDON3D_PROJECT_DB_OK);
lardon3d_project_close(&state);
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
Lardon3DProjectRecoveryEntry entry; size_t count = 0;
const Lardon3DTaskKindRegistry *registry =
lardon3d_task_kind_registry_production();
CHECK(lardon3d_project_list_recoverable(&state, registry, 0, &entry, 1,
&count) == LARDON3D_PROJECT_DB_OK && count == 1);
CHECK(entry.task_id == task_id
&& strcmp(entry.task_kind, LARDON3D_IMAGE_IMPORT_TASK_KIND) == 0
&& entry.task_kind_version == LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION);
Lardon3DProjectRecoverySummary recovery;
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
CHECK(recovery.inspected == 1 && recovery.resumed == 1
&& recovery.skipped == 0 && recovery.failed == 0
&& recovery.published_not_durable == 1);
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
CHECK(lardon3d_task_queue_resume(state.task_queue, task_id));
Lardon3DProjectDbImageImport persisted_parameters;
CHECK(lardon3d_project_db_load_image_import(state.project_db, task_id,
&persisted_parameters) == LARDON3D_PROJECT_DB_OK);
CHECK(strcmp(persisted_parameters.source_path, source) == 0);
Lardon3DImageImportReconstructionContext reconstruction = {
.project_path = state.project_path,
.project_db = state.project_db,
.resource_governor = state.resource_governor,
};
Lardon3DTask *restored = NULL;
char unavailable_source[PATH_MAX];
CHECK(join_path(unavailable_source, root, "source-unavailable"));
CHECK(rename(source, unavailable_source) == 0);
CHECK(lardon3d_task_kind_registry_restore(registry, entry.task_kind,
entry.task_kind_version, &entry.snapshot, &reconstruction, &restored)
== LARDON3D_TASK_KIND_RECONSTRUCTION_FAILED);
CHECK(!restored && rename(unavailable_source, source) == 0);
CHECK(lardon3d_task_kind_registry_restore(registry, entry.task_kind,
entry.task_kind_version, &entry.snapshot, &reconstruction, &restored)
== LARDON3D_TASK_KIND_OK);
CHECK(restored && lardon3d_task_id(restored) == task_id);
CHECK(lardon3d_task_queue_add(state.task_queue, restored, NULL));
CHECK(wait_for_state(state.task_queue, task_id, TASK_COMPLETED, &runtime));
CHECK(runtime.progress == 100);
@ -162,8 +167,8 @@ run_test(void)
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
CHECK(state.task_queue && lardon3d_project_open(&state, "Persistent Import"));
CHECK(lardon3d_project_list_recoverable(&state, registry, 0, &entry, 1,
&count) == LARDON3D_PROJECT_DB_OK && count == 0);
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery)
&& recovery.inspected == 0 && recovery.resumed == 0);
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
lardon3d_project_close(&state);
lardon3d_resource_governor_destroy(state.resource_governor);
@ -172,4 +177,117 @@ run_test(void)
return true;
}
int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; }
static bool
test_selective_capacity_one(void)
{
char root[] = "/tmp/lardon3d-import-recovery-window-XXXXXX";
CHECK(mkdtemp(root));
char missing_source[PATH_MAX], valid_source[PATH_MAX], second_valid_source[PATH_MAX];
char unavailable_source[PATH_MAX];
CHECK(join_path(missing_source, root, "missing-source"));
CHECK(join_path(valid_source, root, "valid-source"));
CHECK(join_path(second_valid_source, root, "second-valid-source"));
CHECK(join_path(unavailable_source, root, "source-unavailable"));
CHECK(mkdir(missing_source, 0700) == 0
&& mkdir(valid_source, 0700) == 0
&& mkdir(second_valid_source, 0700) == 0);
for (unsigned int index = 0; index < 40; ++index) {
char name[32], path[PATH_MAX];
CHECK(snprintf(name, sizeof(name), "missing-%03u.jpg", index) > 0);
CHECK(join_path(path, missing_source, name)
&& write_fixture(path, index));
CHECK(snprintf(name, sizeof(name), "valid-%03u.jpg", index) > 0);
CHECK(join_path(path, valid_source, name)
&& write_fixture(path, index + 100));
CHECK(snprintf(name, sizeof(name), "second-%03u.jpg", index) > 0);
CHECK(join_path(path, second_valid_source, name)
&& write_fixture(path, index + 200));
}
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
Lardon3DAppState state;
lardon3d_app_state_init(&state);
CHECK(setup_runtime(&state, 4));
CHECK(lardon3d_project_create(&state, "Selective Recovery"));
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1)
== 0);
uint64_t missing_id = 0, valid_id = 0, second_valid_id = 0;
CHECK(lardon3d_project_enqueue_image_import(&state, missing_source,
&missing_id));
CHECK(lardon3d_project_enqueue_image_import(&state, valid_source,
&valid_id));
CHECK(lardon3d_project_enqueue_image_import(&state, second_valid_source,
&second_valid_id));
Lardon3DTaskSnapshot snapshot;
CHECK(wait_for_state(state.task_queue, missing_id, TASK_PAUSED, &snapshot));
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_close(&state);
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
CHECK(rename(missing_source, unavailable_source) == 0);
const Lardon3DResourceEstimate held_estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 1,
.desired_cpu_threads = 1,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_IMPORT,
};
Lardon3DResourceSnapshot available = {
.memory_available_bytes = UINT64_MAX,
.cpu_load_1m = 0.0,
};
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *held = NULL;
CHECK(lardon3d_resource_governor_reserve(state.resource_governor,
&available, &held_estimate, &decision, &held));
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
CHECK(state.task_queue
&& lardon3d_project_open(&state, "Selective Recovery"));
Lardon3DProjectRecoverySummary recovery;
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
CHECK(recovery.inspected == 3 && recovery.resumed == 1
&& recovery.skipped == 1 && recovery.failed == 1
&& recovery.queue_full);
CHECK(lardon3d_resource_governor_release(state.resource_governor, held));
lardon3d_task_queue_resources_changed(state.task_queue);
CHECK(wait_for_state(state.task_queue, valid_id, TASK_COMPLETED, &snapshot));
CHECK(snapshot.id == valid_id && snapshot.progress == 100);
CHECK(!lardon3d_task_queue_get(state.task_queue, missing_id, &snapshot));
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_close(&state);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
CHECK(state.task_queue
&& lardon3d_project_open(&state, "Selective Recovery"));
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
CHECK(recovery.inspected == 2 && recovery.resumed == 1
&& recovery.failed == 1);
CHECK(wait_for_state(state.task_queue, second_valid_id, TASK_COMPLETED,
&snapshot));
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_close(&state);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 1);
CHECK(state.task_queue
&& lardon3d_project_open(&state, "Selective Recovery"));
CHECK(lardon3d_project_last_recovery_summary(&state, &recovery));
CHECK(recovery.inspected == 1 && recovery.resumed == 0
&& recovery.failed == 1);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_close(&state);
lardon3d_resource_governor_destroy(state.resource_governor);
CHECK(unsetenv("LARDON3D_PROJECTS_ROOT") == 0);
CHECK(remove_tree(root));
return true;
}
int main(void)
{
return run_test() && test_selective_capacity_one()
? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

@ -10,6 +10,7 @@
#include <lardon3d/project.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
#define CHECK(condition) do { if (!(condition)) { \
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
@ -220,6 +221,33 @@ run_test(void)
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
CHECK(lardon3d_project_list_recoverable(&state, &registry, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE);
Lardon3DHardwareProfile recovery_profile = {
.logical_cpu_count = 2,
.page_size_bytes = 4096,
.memory_total_bytes = UINT64_MAX,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy recovery_policy = {
.maximum_cpu_load_ratio = 1.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
};
state.resource_governor = lardon3d_resource_governor_create(
&recovery_profile, &recovery_policy);
state.task_queue = state.resource_governor
? lardon3d_task_queue_create(state.resource_governor, 1) : NULL;
CHECK(state.task_queue);
Lardon3DProjectRecoverySummary busy_summary;
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY", "1", 1) == 0);
CHECK(lardon3d_project_resume_recoverable_tasks(&state, &registry,
&busy_summary) == LARDON3D_PROJECT_DB_BUSY);
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY") == 0);
CHECK(busy_summary.inspected == 0 && busy_summary.resumed == 0
&& busy_summary.failed == 1);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_resource_governor_destroy(state.resource_governor);
state.resource_governor = NULL;
CHECK(strcmp(entries[0].task_kind, "test.persisted") == 0
&& entries[0].task_kind_version == 1);
Lardon3DTask *restored = NULL;
@ -275,6 +303,25 @@ run_test(void)
== LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION);
CHECK(negative_entries[3].task_id == 6
&& negative_entries[3].status == LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED);
state.resource_governor = lardon3d_resource_governor_create(
&recovery_profile, &recovery_policy);
state.task_queue = state.resource_governor
? lardon3d_task_queue_create(state.resource_governor, 4) : NULL;
CHECK(state.task_queue);
Lardon3DProjectRecoverySummary selective_summary;
CHECK(lardon3d_project_resume_recoverable_tasks(&state, &registry,
&selective_summary) == LARDON3D_PROJECT_DB_OK);
CHECK(selective_summary.inspected == 4 && selective_summary.resumed == 1
&& selective_summary.skipped == 3 && selective_summary.failed == 0);
Lardon3DTaskSnapshot resumed_snapshot;
CHECK(lardon3d_task_queue_get(state.task_queue, 1, &resumed_snapshot));
CHECK(lardon3d_project_resume_recoverable_tasks(&state, &registry,
&selective_summary) == LARDON3D_PROJECT_DB_OK);
CHECK(selective_summary.resumed == 0 && selective_summary.skipped == 4);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_resource_governor_destroy(state.resource_governor);
state.resource_governor = NULL;
lardon3d_task_destroy(legacy);
lardon3d_task_destroy(future_kind);
lardon3d_task_destroy(unknown);

View file

@ -233,7 +233,8 @@ run_test(void)
Lardon3DTask *duplicate = lardon3d_task_create(
"Doublon", &estimate, queue_callback, &restored_work);
CHECK(duplicate && lardon3d_task_assign_id(duplicate, 100));
CHECK(!lardon3d_task_queue_add(queue, duplicate, NULL));
CHECK(lardon3d_task_queue_try_add_ex(queue, duplicate, NULL)
== LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID);
lardon3d_task_destroy(duplicate);
CHECK(wait_terminal(queue, 100, &snapshot));
CHECK(lardon3d_task_queue_remove(queue, 100));