diff --git a/.opencode/agents/lardon-architect.md b/.opencode/agents/lardon-architect.md index f6412b1..5802d60 100644 --- a/.opencode/agents/lardon-architect.md +++ b/.opencode/agents/lardon-architect.md @@ -5,7 +5,12 @@ model: opencode/nemotron-3-ultra-free temperature: 0.1 permission: edit: deny - bash: deny + bash: + "*": deny + "rg *": allow + "git grep*": allow + "git diff*": allow + "sed -n *": allow task: deny --- diff --git a/.opencode/agents/lardon-build-backup-router.md b/.opencode/agents/lardon-build-backup-router.md new file mode 100644 index 0000000..649ae57 --- /dev/null +++ b/.opencode/agents/lardon-build-backup-router.md @@ -0,0 +1,79 @@ +--- +description: Reprend un build complexe via OpenRouter lorsque Google ou Zen sont indisponibles +mode: subagent +model: openrouter/nvidia/nemotron-3-ultra-550b-a55b:free +temperature: 0.1 +permission: + edit: + "*": allow + "scan3d/**": deny + bash: + "*": deny + "rg *": allow + "git grep*": allow + "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow + "CC=clang meson setup *": allow + "meson setup *": allow + "meson compile *": allow + "meson test *": allow + "ninja *": allow + "git diff --check*": allow + task: deny +--- + +Tu es le deuxième agent de secours pour l'implémentation de Lardon3D. + +Tu interviens uniquement lorsqu'un ticket est déjà préparé et que le build +principal ou le premier backup sont indisponibles. + +Commence par lire : + +- `.opencode/work/current_ticket.md` ; +- le diff courant ; +- uniquement les fichiers explicitement cités dans le handoff. + +Ne refais pas l'exploration ou l'architecture si le handoff contient déjà les +informations nécessaires. + +Ne charge jamais tout le dépôt. + +Ne lance jamais : + +- `pwd` +- `ls` +- `find` +- glob global +- lecture complète de la documentation + +Respecte strictement : + +- l'objectif du ticket ; +- les contraintes ; +- les API existantes ; +- les invariants ; +- le périmètre de fichiers indiqué. + +Ne crée aucune fonctionnalité supplémentaire. + +Ne modifie jamais `scan3d/`. + +Ne fais aucun commit. +Ne fais aucun push. + +Après implémentation : + +- exécute uniquement les validations demandées par le ticket ; +- ne tente pas de corriger un autre problème découvert hors périmètre ; +- signale-le seulement. + +À la fin, rapporte uniquement : + +- modification exacte ; +- fichiers modifiés ; +- validations exécutées ; +- erreurs éventuelles ; +- limites restantes ; +- prochaine action sûre. diff --git a/.opencode/agents/lardon-build-backup.md b/.opencode/agents/lardon-build-backup.md index 1ef9a56..42721da 100644 --- a/.opencode/agents/lardon-build-backup.md +++ b/.opencode/agents/lardon-build-backup.md @@ -1,12 +1,26 @@ --- description: Reprend les tickets complexes interrompus sans refaire l'analyse mode: subagent -model: opencode/nemotron-3-ultra-free +model: opencode/deepseek-v4-flash-free temperature: 0.1 permission: edit: "*": allow "scan3d/**": deny + bash: + "*": deny + "rg *": allow + "git grep*": allow + "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow + "CC=clang meson setup *": allow + "meson setup *": allow + "meson compile *": allow + "meson test *": allow + "ninja *": allow + "git diff --check*": allow task: deny --- diff --git a/.opencode/agents/lardon-build-light.md b/.opencode/agents/lardon-build-light.md index 21636da..a004d11 100644 --- a/.opencode/agents/lardon-build-light.md +++ b/.opencode/agents/lardon-build-light.md @@ -7,6 +7,20 @@ permission: edit: "*": allow "scan3d/**": deny + bash: + "*": deny + "rg *": allow + "git grep*": allow + "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow + "CC=clang meson setup *": allow + "meson setup *": allow + "meson compile *": allow + "meson test *": allow + "ninja *": allow + "git diff --check*": allow task: deny --- diff --git a/.opencode/agents/lardon-build.md b/.opencode/agents/lardon-build.md index d7bfbb9..a7595e1 100644 --- a/.opencode/agents/lardon-build.md +++ b/.opencode/agents/lardon-build.md @@ -1,12 +1,26 @@ --- description: Implémente seul les tickets complexes Lardon3D mode: subagent -model: opencode/deepseek-v4-flash-free +model: google/gemini-3.6-flash temperature: 0.1 permission: edit: "*": allow "scan3d/**": deny + bash: + "*": deny + "rg *": allow + "git grep*": allow + "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow + "CC=clang meson setup *": allow + "meson setup *": allow + "meson compile *": allow + "meson test *": allow + "ninja *": allow + "git diff --check*": allow task: deny --- diff --git a/.opencode/agents/lardon-concurrency.md b/.opencode/agents/lardon-concurrency.md index e48c667..34302be 100644 --- a/.opencode/agents/lardon-concurrency.md +++ b/.opencode/agents/lardon-concurrency.md @@ -7,8 +7,12 @@ permission: edit: deny bash: "*": deny - "git diff*": allow "rg *": allow + "git grep*": allow + "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow task: deny --- diff --git a/.opencode/agents/lardon-explore.md b/.opencode/agents/lardon-explore.md index 559b777..454bed7 100644 --- a/.opencode/agents/lardon-explore.md +++ b/.opencode/agents/lardon-explore.md @@ -9,6 +9,9 @@ permission: "*": deny "rg *": allow "git grep*": allow + "git status*": allow + "git diff*": allow + "sed -n *": allow task: deny --- diff --git a/.opencode/agents/lardon-orchestrator.md b/.opencode/agents/lardon-orchestrator.md index 3ad1672..2b8a62a 100644 --- a/.opencode/agents/lardon-orchestrator.md +++ b/.opencode/agents/lardon-orchestrator.md @@ -16,6 +16,7 @@ permission: "*": deny "lardon-build": allow "lardon-build-backup": allow + "lardon-build-backup-router": allow "lardon-build-light": allow "lardon-architect": allow "lardon-explore": allow @@ -70,6 +71,21 @@ Cette règle s’applique même si le ticket ne crée aucun nouveau thread. - fichiers concernés ; - API et invariants ; - tests requis. + + Chaîne d'implémentation : + + 1. Utiliser `lardon-build` pour l'implémentation normale. + 2. Si `lardon-build` échoue pour indisponibilité du fournisseur, quota ou erreur + 429/503, conserver le handoff et utiliser `lardon-build-backup`. + 3. Si le premier backup est lui-même indisponible, utiliser + `lardon-build-backup-router`. + 4. Ne jamais recommencer l'exploration ou l'architecture lors d'un fallback si + le handoff contient déjà les informations nécessaires. + 5. Ne jamais basculer vers un modèle payant. + + Un fallback ne doit modifier ni l'objectif, ni le périmètre, ni les invariants + du ticket. + 7. Mettre à jour le handoff après chaque phase importante. Le répertoire `.opencode/work/` est préparé localement. Ne vérifie pas son diff --git a/.opencode/agents/lardon-review.md b/.opencode/agents/lardon-review.md index 3110f5d..d15a5e5 100644 --- a/.opencode/agents/lardon-review.md +++ b/.opencode/agents/lardon-review.md @@ -7,8 +7,12 @@ permission: edit: deny bash: "*": deny - "git diff*": allow + "rg *": allow + "git grep*": allow "git status*": allow + "git diff*": allow + "git log*": allow + "sed -n *": allow task: deny --- diff --git a/.opencode/agents/lardon-tests.md b/.opencode/agents/lardon-tests.md index 9eff792..2b81915 100644 --- a/.opencode/agents/lardon-tests.md +++ b/.opencode/agents/lardon-tests.md @@ -8,10 +8,11 @@ permission: bash: "*": deny "CC=clang meson setup *": allow + "meson setup *": allow "meson compile *": allow "meson test *": allow "ninja *": allow - "git diff --check*": allow + "git diff*": allow "git status*": allow task: deny --- diff --git a/include/lardon3d/task.h b/include/lardon3d/task.h index 445237e..6573c72 100644 --- a/include/lardon3d/task.h +++ b/include/lardon3d/task.h @@ -83,8 +83,11 @@ bool lardon3d_task_execution_contract( ); /* Libère la réservation courante, en obtient une nouvelle auprès du gouverneur * et met à jour le contrat. À appeler uniquement depuis le callback en cours - * d'exécution. Retourne false si pause/annulation est demandée ou si le - * gouverneur répond WAIT/REJECT (la tâche passe alors en échec). */ + * d'exécution. Une réponse WAIT du gouverneur est une indisponibilité + * temporaire : la fonction attend un changement de ressources puis retente + * l'admission sans échouer la tâche. Retourne false si la tâche est annulée + * (TASK_CANCELLED), si le gouverneur répond REJECT ou en cas d'erreur interne + * (TASK_FAILED). */ bool lardon3d_task_sequence_break( Lardon3DTask *task, Lardon3DResourceGovernor *governor, diff --git a/src/task.c b/src/task.c index 17400da..c3250ce 100644 --- a/src/task.c +++ b/src/task.c @@ -278,6 +278,11 @@ lardon3d_task_checkpoint(Lardon3DTask *task) return continuing; } +enum { + /* Attente bornée entre deux tentatives d'admission : 50 ms. */ + LARDON3D_SEQUENCE_ADMISSION_WAIT_NS = 50000000ULL, +}; + bool lardon3d_task_sequence_break( Lardon3DTask *task, @@ -302,6 +307,7 @@ lardon3d_task_sequence_break( (void)pthread_cond_wait(&task->condition, &task->mutex); } if (task->cancel_requested) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); (void)pthread_mutex_unlock(&task->mutex); return false; } @@ -314,95 +320,121 @@ lardon3d_task_sequence_break( (void)lardon3d_resource_governor_release(governor, previous); } - Lardon3DResourceDecision decision; - Lardon3DResourceReservation *next = NULL; - bool reserved = lardon3d_resource_governor_reserve_available( - governor, - &task->estimate, - &decision, - &next - ); + for (;;) { + /* Vérifier pause et annulation avant chaque tentative d'admission. */ + (void)pthread_mutex_lock(&task->mutex); + while (task->pause_requested && !task->cancel_requested) { + task->state = TASK_PAUSED; + copy_text(task->message, sizeof(task->message), "Tâche en pause."); + (void)pthread_cond_broadcast(&task->condition); + (void)pthread_cond_wait(&task->condition, &task->mutex); + } + if (task->cancel_requested) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); + (void)pthread_mutex_unlock(&task->mutex); + return false; + } + task->state = TASK_RUNNING; + (void)pthread_mutex_unlock(&task->mutex); - (void)pthread_mutex_lock(&task->mutex); - if (task->cancel_requested) { - if (next) { - (void)lardon3d_resource_governor_release(governor, next); - } - finish_locked( - task, - TASK_CANCELLED, - "Tâche annulée pendant sequence_break." - ); - (void)pthread_mutex_unlock(&task->mutex); - return false; - } - while (task->pause_requested && !task->cancel_requested) { - task->state = TASK_PAUSED; - copy_text(task->message, sizeof(task->message), "Tâche en pause."); - (void)pthread_cond_broadcast(&task->condition); - (void)pthread_cond_wait(&task->condition, &task->mutex); - } - if (task->cancel_requested) { - if (next) { - (void)lardon3d_resource_governor_release(governor, next); - } - finish_locked(task, TASK_CANCELLED, "Tâche annulée."); - (void)pthread_mutex_unlock(&task->mutex); - return false; - } - if (!reserved || !next) { - task->current_reservation = NULL; - finish_locked( - task, - TASK_FAILED, - "Impossible de réserver les ressources de la séquence." - ); - (void)pthread_mutex_unlock(&task->mutex); - return false; - } - if (decision.kind == LARDON3D_RESOURCE_WAIT - || decision.kind == LARDON3D_RESOURCE_REJECT) { - task->current_reservation = NULL; - finish_locked( - task, - TASK_FAILED, - decision.reason[0] ? decision.reason - : "Ressources impossibles à réserver." - ); - (void)pthread_mutex_unlock(&task->mutex); - return false; - } - Lardon3DResourceReservationInfo information; - if (!lardon3d_resource_reservation_get_active( + uint64_t generation = lardon3d_resource_governor_generation(governor); + Lardon3DResourceDecision decision; + Lardon3DResourceReservation *next = NULL; + bool admitted = lardon3d_resource_governor_reserve_available( governor, - next, - &information - )) { - (void)lardon3d_resource_governor_release(governor, next); - task->current_reservation = NULL; - finish_locked( - task, - TASK_FAILED, - "Réservation de séquence invalide." + &task->estimate, + &decision, + &next ); - (void)pthread_mutex_unlock(&task->mutex); - return false; + if (!admitted) { + /* Erreur interne : échec d'allocation ou d'instantané. */ + (void)pthread_mutex_lock(&task->mutex); + task->current_reservation = NULL; + finish_locked( + task, + TASK_FAILED, + "Impossible de réserver les ressources de la séquence." + ); + (void)pthread_mutex_unlock(&task->mutex); + return false; + } + switch (decision.kind) { + case LARDON3D_RESOURCE_START: + case LARDON3D_RESOURCE_REDUCE_BATCH: { + if (!next) { + (void)pthread_mutex_lock(&task->mutex); + task->current_reservation = NULL; + finish_locked( + task, + TASK_FAILED, + "Réservation de séquence invalide." + ); + (void)pthread_mutex_unlock(&task->mutex); + return false; + } + Lardon3DResourceReservationInfo information; + if (!lardon3d_resource_reservation_get_active( + governor, + next, + &information + )) { + (void)lardon3d_resource_governor_release(governor, next); + (void)pthread_mutex_lock(&task->mutex); + task->current_reservation = NULL; + finish_locked( + task, + TASK_FAILED, + "Réservation de séquence invalide." + ); + (void)pthread_mutex_unlock(&task->mutex); + return false; + } + (void)pthread_mutex_lock(&task->mutex); + task->current_reservation = next; + task->contract = (Lardon3DTaskExecutionContract) { + .batch_size = information.batch_size, + .memory_bytes = information.memory_bytes, + .gpu_memory_bytes = information.gpu_memory_bytes, + .cpu_threads = information.cpu_threads, + .gpu_slots = information.gpu_slots, + .io_slots = information.io_slots, + }; + task->has_contract = true; + ++task->sequence_count; + *out_reservation = next; + *out_contract = task->contract; + (void)pthread_mutex_unlock(&task->mutex); + return true; + } + case LARDON3D_RESOURCE_REJECT: + if (next) { + (void)lardon3d_resource_governor_release(governor, next); + } + (void)pthread_mutex_lock(&task->mutex); + task->current_reservation = NULL; + finish_locked( + task, + TASK_FAILED, + decision.reason[0] ? decision.reason + : "Ressources impossibles à réserver." + ); + (void)pthread_mutex_unlock(&task->mutex); + return false; + case LARDON3D_RESOURCE_WAIT: + default: + /* Indisponibilité temporaire : ne pas échouer, attendre un + * changement de ressources puis retenter l'admission. */ + if (next) { + (void)lardon3d_resource_governor_release(governor, next); + } + (void)lardon3d_resource_governor_wait_for_change( + governor, + generation, + LARDON3D_SEQUENCE_ADMISSION_WAIT_NS + ); + break; + } } - task->current_reservation = next; - task->contract = (Lardon3DTaskExecutionContract) { - .batch_size = information.batch_size, - .memory_bytes = information.memory_bytes, - .gpu_memory_bytes = information.gpu_memory_bytes, - .cpu_threads = information.cpu_threads, - .gpu_slots = information.gpu_slots, - .io_slots = information.io_slots, - }; - task->has_contract = true; - ++task->sequence_count; - *out_reservation = next; - *out_contract = task->contract; - (void)pthread_mutex_unlock(&task->mutex); - return true; } unsigned int diff --git a/tests/test_sequential_task.c b/tests/test_sequential_task.c index 94d153f..f0dcbe8 100644 --- a/tests/test_sequential_task.c +++ b/tests/test_sequential_task.c @@ -28,7 +28,14 @@ typedef struct { typedef struct { Lardon3DResourceGovernor *governor; -} WaitContext; + pthread_mutex_t sync_mutex; + pthread_cond_t sync_cond; + bool in_break; +} WaitRetryContext; + +typedef struct { + Lardon3DResourceGovernor *governor; +} RejectContext; typedef struct { Lardon3DResourceGovernor *governor; @@ -102,9 +109,27 @@ seq_callback(Lardon3DTask *task, void *userdata) } static bool -wait_callback(Lardon3DTask *task, void *userdata) +wait_retry_callback(Lardon3DTask *task, void *userdata) { - WaitContext *ctx = userdata; + WaitRetryContext *ctx = userdata; + (void)pthread_mutex_lock(&ctx->sync_mutex); + ctx->in_break = true; + (void)pthread_cond_broadcast(&ctx->sync_cond); + (void)pthread_mutex_unlock(&ctx->sync_mutex); + Lardon3DResourceReservation *reservation = NULL; + Lardon3DTaskExecutionContract contract; + return lardon3d_task_sequence_break( + task, + ctx->governor, + &reservation, + &contract + ); +} + +static bool +reject_callback(Lardon3DTask *task, void *userdata) +{ + RejectContext *ctx = userdata; Lardon3DResourceReservation *reservation = NULL; Lardon3DTaskExecutionContract contract; return lardon3d_task_sequence_break( @@ -247,7 +272,7 @@ run_sequential_test(void) } static bool -run_wait_test(void) +run_wait_retry_test(void) { const Lardon3DResourceEstimate estimate = { .minimum_batch_size = 1, @@ -274,7 +299,6 @@ run_wait_test(void) Lardon3DResourceSnapshot resource_snapshot = { .memory_available_bytes = UINT64_MAX, .cpu_load_1m = 0.0, - .io_pressure_known = false, }; Lardon3DResourceDecision decision; Lardon3DResourceReservation *reservation; @@ -285,11 +309,255 @@ run_wait_test(void) &decision, &reservation )); - WaitContext ctx = {.governor = governor}; + CHECK(reservation); + WaitRetryContext ctx = {.governor = governor}; + CHECK(pthread_mutex_init(&ctx.sync_mutex, NULL) == 0); + CHECK(pthread_cond_init(&ctx.sync_cond, NULL) == 0); Lardon3DTask *task = lardon3d_task_create( - "Attente", + "Attente puis reprise", &estimate, - wait_callback, + wait_retry_callback, + &ctx + ); + CHECK(task); + StartContext start = {task, governor, reservation}; + pthread_t thread; + CHECK(pthread_create(&thread, NULL, start_task, &start) == 0); + (void)pthread_mutex_lock(&ctx.sync_mutex); + while (!ctx.in_break) { + (void)pthread_cond_wait(&ctx.sync_cond, &ctx.sync_mutex); + } + (void)pthread_mutex_unlock(&ctx.sync_mutex); + /* Tant que la politique reste bloquante, le gouverneur répond WAIT : + * la tâche doit rester en cours et ne jamais passer en échec. */ + Lardon3DTaskSnapshot snapshot; + for (size_t attempt = 0; attempt < 50; ++attempt) { + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_RUNNING); + short_pause(1000000); + } + CHECK(lardon3d_task_sequence_count(task) == 0); + /* Lever la contrainte : la prochaine tentative doit réussir. */ + Lardon3DResourcePolicy allowed = policy; + allowed.maximum_io_pressure_avg10 = 100.0; + CHECK(lardon3d_resource_governor_set_policy(governor, &allowed)); + CHECK(lardon3d_task_join(task)); + void *thread_result; + CHECK(pthread_join(thread, &thread_result) == 0); + CHECK((uintptr_t)thread_result == 1); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_COMPLETED); + CHECK(lardon3d_task_sequence_count(task) == 1); + CHECK(lardon3d_resource_governor_reservation_count(governor) == 0); + lardon3d_task_destroy(task); + (void)pthread_cond_destroy(&ctx.sync_cond); + (void)pthread_mutex_destroy(&ctx.sync_mutex); + lardon3d_resource_governor_destroy(governor); + return true; +} + +static bool +run_cancel_during_wait_test(void) +{ + const Lardon3DResourceEstimate estimate = { + .minimum_batch_size = 1, + .maximum_batch_size = 100, + .desired_cpu_threads = 1, + .desired_io_slots = 1, + }; + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 16, + .page_size_bytes = 4096, + .memory_total_bytes = UINT64_MAX, + .cpu_architecture = "test", + }; + Lardon3DResourcePolicy policy = { + .maximum_cpu_load_ratio = 1.0, + .maximum_io_pressure_avg10 = 0.0, + .io_slot_capacity = 1, + }; + Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create( + &profile, + &policy + ); + CHECK(governor); + Lardon3DResourceSnapshot resource_snapshot = { + .memory_available_bytes = UINT64_MAX, + .cpu_load_1m = 0.0, + }; + Lardon3DResourceDecision decision; + Lardon3DResourceReservation *reservation; + CHECK(lardon3d_resource_governor_reserve( + governor, + &resource_snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(reservation); + WaitRetryContext ctx = {.governor = governor}; + CHECK(pthread_mutex_init(&ctx.sync_mutex, NULL) == 0); + CHECK(pthread_cond_init(&ctx.sync_cond, NULL) == 0); + Lardon3DTask *task = lardon3d_task_create( + "Annulation pendant attente", + &estimate, + wait_retry_callback, + &ctx + ); + CHECK(task); + StartContext start = {task, governor, reservation}; + pthread_t thread; + CHECK(pthread_create(&thread, NULL, start_task, &start) == 0); + (void)pthread_mutex_lock(&ctx.sync_mutex); + while (!ctx.in_break) { + (void)pthread_cond_wait(&ctx.sync_cond, &ctx.sync_mutex); + } + (void)pthread_mutex_unlock(&ctx.sync_mutex); + /* Annuler pendant que la tâche attend les ressources. */ + lardon3d_task_request_cancel(task); + CHECK(lardon3d_task_join(task)); + CHECK(pthread_join(thread, NULL) == 0); + Lardon3DTaskSnapshot snapshot; + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_CANCELLED); + CHECK(lardon3d_resource_governor_reservation_count(governor) == 0); + lardon3d_task_destroy(task); + (void)pthread_cond_destroy(&ctx.sync_cond); + (void)pthread_mutex_destroy(&ctx.sync_mutex); + lardon3d_resource_governor_destroy(governor); + return true; +} + +static bool +run_pause_during_wait_test(void) +{ + const Lardon3DResourceEstimate estimate = { + .minimum_batch_size = 1, + .maximum_batch_size = 100, + .desired_cpu_threads = 1, + .desired_io_slots = 1, + }; + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 16, + .page_size_bytes = 4096, + .memory_total_bytes = UINT64_MAX, + .cpu_architecture = "test", + }; + Lardon3DResourcePolicy policy = { + .maximum_cpu_load_ratio = 1.0, + .maximum_io_pressure_avg10 = 0.0, + .io_slot_capacity = 1, + }; + Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create( + &profile, + &policy + ); + CHECK(governor); + Lardon3DResourceSnapshot resource_snapshot = { + .memory_available_bytes = UINT64_MAX, + .cpu_load_1m = 0.0, + }; + Lardon3DResourceDecision decision; + Lardon3DResourceReservation *reservation; + CHECK(lardon3d_resource_governor_reserve( + governor, + &resource_snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(reservation); + WaitRetryContext ctx = {.governor = governor}; + CHECK(pthread_mutex_init(&ctx.sync_mutex, NULL) == 0); + CHECK(pthread_cond_init(&ctx.sync_cond, NULL) == 0); + Lardon3DTask *task = lardon3d_task_create( + "Pause pendant attente", + &estimate, + wait_retry_callback, + &ctx + ); + CHECK(task); + StartContext start = {task, governor, reservation}; + pthread_t thread; + CHECK(pthread_create(&thread, NULL, start_task, &start) == 0); + (void)pthread_mutex_lock(&ctx.sync_mutex); + while (!ctx.in_break) { + (void)pthread_cond_wait(&ctx.sync_cond, &ctx.sync_mutex); + } + (void)pthread_mutex_unlock(&ctx.sync_mutex); + /* Mettre en pause pendant l'attente, puis reprendre. */ + CHECK(lardon3d_task_pause(task)); + CHECK(wait_for_state(task, TASK_PAUSED)); + CHECK(lardon3d_task_resume(task)); + CHECK(wait_for_state(task, TASK_RUNNING)); + /* Lever la contrainte : la tâche doit reprendre puis réussir. */ + Lardon3DResourcePolicy allowed = policy; + allowed.maximum_io_pressure_avg10 = 100.0; + CHECK(lardon3d_resource_governor_set_policy(governor, &allowed)); + CHECK(lardon3d_task_join(task)); + CHECK(pthread_join(thread, NULL) == 0); + Lardon3DTaskSnapshot snapshot; + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_COMPLETED); + CHECK(lardon3d_task_sequence_count(task) == 1); + CHECK(lardon3d_resource_governor_reservation_count(governor) == 0); + lardon3d_task_destroy(task); + (void)pthread_cond_destroy(&ctx.sync_cond); + (void)pthread_mutex_destroy(&ctx.sync_mutex); + lardon3d_resource_governor_destroy(governor); + return true; +} + +static bool +run_reject_test(void) +{ + const Lardon3DResourceEstimate valid_estimate = { + .minimum_batch_size = 1, + .maximum_batch_size = 1, + .desired_cpu_threads = 1, + }; + const Lardon3DResourceEstimate reject_estimate = { + .memory_fixed_bytes = UINT64_MAX, + .memory_bytes_per_item = 1, + .minimum_batch_size = 1, + .maximum_batch_size = 1, + .desired_cpu_threads = 1, + }; + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 16, + .page_size_bytes = 4096, + .memory_total_bytes = UINT64_MAX, + .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 + ); + CHECK(governor); + Lardon3DResourceSnapshot resource_snapshot = { + .memory_available_bytes = UINT64_MAX, + .cpu_load_1m = 0.0, + }; + Lardon3DResourceDecision decision; + Lardon3DResourceReservation *reservation; + CHECK(lardon3d_resource_governor_reserve( + governor, + &resource_snapshot, + &valid_estimate, + &decision, + &reservation + )); + CHECK(reservation); + RejectContext ctx = {.governor = governor}; + Lardon3DTask *task = lardon3d_task_create( + "Refus gouverneur", + &reject_estimate, + reject_callback, &ctx ); CHECK(task); @@ -303,6 +571,52 @@ run_wait_test(void) return true; } +static bool +run_invalid_break_test(void) +{ + const Lardon3DResourceEstimate estimate = { + .minimum_batch_size = 1, + .maximum_batch_size = 1, + .desired_cpu_threads = 1, + }; + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 4, + .page_size_bytes = 4096, + .memory_total_bytes = UINT64_MAX, + .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 + ); + CHECK(governor); + Lardon3DTask *task = lardon3d_task_create( + "Sans réservation", + &estimate, + wait_retry_callback, + NULL + ); + CHECK(task); + Lardon3DResourceReservation *reservation = NULL; + Lardon3DTaskExecutionContract contract; + CHECK(!lardon3d_task_sequence_break(task, governor, &reservation, &contract)); + CHECK(!lardon3d_task_sequence_break(NULL, governor, &reservation, &contract)); + CHECK(!lardon3d_task_sequence_break(task, NULL, &reservation, &contract)); + CHECK(!lardon3d_task_sequence_break(task, governor, NULL, &contract)); + CHECK(!lardon3d_task_sequence_break(task, governor, &reservation, NULL)); + Lardon3DTaskSnapshot snapshot; + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_PENDING); + lardon3d_task_destroy(task); + lardon3d_resource_governor_destroy(governor); + return true; +} + static bool run_cancel_test(void) { @@ -382,11 +696,23 @@ main(void) if (!run_sequential_test()) { return EXIT_FAILURE; } - if (!run_wait_test()) { + if (!run_wait_retry_test()) { return EXIT_FAILURE; } if (!run_cancel_test()) { return EXIT_FAILURE; } + if (!run_cancel_during_wait_test()) { + return EXIT_FAILURE; + } + if (!run_pause_during_wait_test()) { + return EXIT_FAILURE; + } + if (!run_reject_test()) { + return EXIT_FAILURE; + } + if (!run_invalid_break_test()) { + return EXIT_FAILURE; + } return EXIT_SUCCESS; } \ No newline at end of file