feat(governor): add thread-safe wait for reservation changes

This commit is contained in:
fy59 2026-08-07 08:03:56 +02:00
parent a986ad0add
commit a716ef16bf
3 changed files with 279 additions and 1 deletions

View file

@ -176,6 +176,14 @@ bool lardon3d_resource_governor_availability(
const Lardon3DResourceSnapshot *snapshot,
Lardon3DResourceAvailability *availability
);
uint64_t lardon3d_resource_governor_generation(
Lardon3DResourceGovernor *governor
);
bool lardon3d_resource_governor_wait_for_change(
Lardon3DResourceGovernor *governor,
uint64_t observed_generation,
uint64_t timeout_ns
);
const char *lardon3d_resource_decision_name(
Lardon3DResourceDecisionKind kind
);

View file

@ -1,3 +1,4 @@
#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
@ -13,6 +14,8 @@ struct Lardon3DResourceReservation {
struct Lardon3DResourceGovernor {
pthread_mutex_t mutex;
pthread_cond_t cond;
uint64_t generation;
Lardon3DHardwareProfile profile;
Lardon3DResourcePolicy policy;
uint64_t next_reservation_id;
@ -203,6 +206,26 @@ lardon3d_resource_governor_create(
free(governor);
return NULL;
}
pthread_condattr_t attr;
if (pthread_condattr_init(&attr) != 0) {
(void)pthread_mutex_destroy(&governor->mutex);
free(governor);
return NULL;
}
if (pthread_condattr_setclock(&attr, CLOCK_MONOTONIC) != 0) {
(void)pthread_condattr_destroy(&attr);
(void)pthread_mutex_destroy(&governor->mutex);
free(governor);
return NULL;
}
if (pthread_cond_init(&governor->cond, &attr) != 0) {
(void)pthread_condattr_destroy(&attr);
(void)pthread_mutex_destroy(&governor->mutex);
free(governor);
return NULL;
}
(void)pthread_condattr_destroy(&attr);
governor->generation = 0;
governor->profile = *profile;
governor->policy = *policy;
governor->next_reservation_id = 1;
@ -227,6 +250,7 @@ lardon3d_resource_governor_destroy(Lardon3DResourceGovernor *governor)
free(reservation);
reservation = next;
}
(void)pthread_cond_destroy(&governor->cond);
(void)pthread_mutex_destroy(&governor->mutex);
free(governor);
}
@ -347,6 +371,62 @@ lardon3d_resource_governor_availability(
return success;
}
uint64_t
lardon3d_resource_governor_generation(
Lardon3DResourceGovernor *governor
)
{
if (!governor) {
return 0;
}
(void)pthread_mutex_lock(&governor->mutex);
uint64_t generation = governor->generation;
(void)pthread_mutex_unlock(&governor->mutex);
return generation;
}
bool
lardon3d_resource_governor_wait_for_change(
Lardon3DResourceGovernor *governor,
uint64_t observed_generation,
uint64_t timeout_ns
)
{
if (!governor) {
return false;
}
struct timespec deadline;
if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) {
return false;
}
deadline.tv_sec += (time_t)(timeout_ns / 1000000000ULL);
uint64_t remainder = timeout_ns % 1000000000ULL;
deadline.tv_nsec += (long)remainder;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_sec += 1;
deadline.tv_nsec -= 1000000000L;
}
(void)pthread_mutex_lock(&governor->mutex);
bool changed = false;
while (governor->generation == observed_generation) {
int result = pthread_cond_timedwait(
&governor->cond,
&governor->mutex,
&deadline
);
if (result == ETIMEDOUT) {
break;
}
if (result != 0) {
(void)pthread_mutex_unlock(&governor->mutex);
return false;
}
}
changed = governor->generation != observed_generation;
(void)pthread_mutex_unlock(&governor->mutex);
return changed;
}
static void
evaluate_locked(
Lardon3DResourceGovernor *governor,
@ -554,6 +634,8 @@ lardon3d_resource_governor_reserve(
governor->io_slots_reserved += decision->io_slots;
++governor->active_count;
*reservation = created;
++governor->generation;
(void)pthread_cond_broadcast(&governor->cond);
(void)pthread_mutex_unlock(&governor->mutex);
return true;
}
@ -635,6 +717,8 @@ lardon3d_resource_governor_release(
current->information.state = LARDON3D_RESERVATION_RELEASED;
current->next = governor->released;
governor->released = current;
++governor->generation;
(void)pthread_cond_broadcast(&governor->cond);
(void)pthread_mutex_unlock(&governor->mutex);
return true;
}

View file

@ -25,6 +25,26 @@ typedef struct {
atomic_bool *failed;
} ThreadContext;
typedef struct {
Lardon3DResourceGovernor *governor;
uint64_t observed_generation;
uint64_t timeout_ns;
atomic_bool *result;
} WaitContext;
static void *
wait_for_change_thread(void *argument)
{
WaitContext *context = argument;
bool changed = lardon3d_resource_governor_wait_for_change(
context->governor,
context->observed_generation,
context->timeout_ns
);
atomic_store(context->result, changed);
return NULL;
}
static void *
decide_repeatedly(void *argument)
{
@ -207,8 +227,174 @@ run_test(void)
return true;
}
static bool
run_generation_test(void)
{
Lardon3DHardwareProfile profile = {
.logical_cpu_count = 16,
.page_size_bytes = 4096,
.memory_total_bytes = GIBIBYTES(16),
.cpu_architecture = "test",
};
Lardon3DResourcePolicy policy = {
.system_memory_reserve_bytes = GIBIBYTES(2),
.gpu_memory_reserve_bytes = 0,
.system_cpu_reserve = 1,
.maximum_cpu_load_ratio = 0.90,
.maximum_io_pressure_avg10 = 80.0,
.io_slot_capacity = 8,
};
Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create(
&profile,
&policy
);
CHECK(governor);
/* Test 1 : génération initiale = 0. */
CHECK(lardon3d_resource_governor_generation(governor) == 0);
Lardon3DResourceSnapshot snapshot = {
.memory_available_bytes = GIBIBYTES(10),
.cpu_load_1m = 2.0,
};
Lardon3DResourceEstimate estimate = {
.memory_bytes_per_item = GIBIBYTES(1),
.minimum_batch_size = 2,
.maximum_batch_size = 8,
.desired_cpu_threads = 4,
.task_class = LARDON3D_RESOURCE_TASK_GENERAL,
};
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *reservation = NULL;
/* Test 2 : WAIT ne change pas la génération. */
snapshot.cpu_load_1m = 15.0;
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(decision.kind == LARDON3D_RESOURCE_WAIT);
CHECK(reservation == NULL);
CHECK(lardon3d_resource_governor_generation(governor) == 0);
snapshot.cpu_load_1m = 2.0;
/* Test 3 : REJECT ne change pas la génération. */
estimate.memory_bytes_per_item = GIBIBYTES(8);
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(decision.kind == LARDON3D_RESOURCE_REJECT);
CHECK(reservation == NULL);
CHECK(lardon3d_resource_governor_generation(governor) == 0);
estimate.memory_bytes_per_item = GIBIBYTES(1);
/* Test 4 : création d'une réservation change la génération (0 -> 1). */
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(decision.kind == LARDON3D_RESOURCE_START
|| decision.kind == LARDON3D_RESOURCE_REDUCE_BATCH);
CHECK(reservation != NULL);
CHECK(lardon3d_resource_governor_generation(governor) == 1);
/* Test 5 : libération change la génération (1 -> 2). */
CHECK(lardon3d_resource_governor_release(governor, reservation));
CHECK(lardon3d_resource_governor_generation(governor) == 2);
/* Test 6 : double libération refusée ne change pas la génération. */
CHECK(!lardon3d_resource_governor_release(governor, reservation));
CHECK(lardon3d_resource_governor_generation(governor) == 2);
/* Test 7 : un thread attendant est réveillé après une libération. */
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(reservation != NULL);
uint64_t generation = lardon3d_resource_governor_generation(governor);
atomic_bool wait_result = false;
WaitContext wait_context = {
.governor = governor,
.observed_generation = generation,
.timeout_ns = 5000000000ULL,
.result = &wait_result,
};
pthread_t waiter;
CHECK(pthread_create(&waiter, NULL, wait_for_change_thread, &wait_context) == 0);
CHECK(lardon3d_resource_governor_release(governor, reservation));
CHECK(pthread_join(waiter, NULL) == 0);
CHECK(atomic_load(&wait_result) == true);
/* Test 8 : l'attente expire proprement (timeout court). */
generation = lardon3d_resource_governor_generation(governor);
CHECK(!lardon3d_resource_governor_wait_for_change(
governor,
generation,
1000000ULL
));
/* Test 9 : une décision WAIT ne modifie pas la génération et
* wait_for_change() expire sans faux changement. */
generation = lardon3d_resource_governor_generation(governor);
snapshot.cpu_load_1m = 15.0;
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(decision.kind == LARDON3D_RESOURCE_WAIT);
CHECK(reservation == NULL);
snapshot.cpu_load_1m = 2.0;
CHECK(lardon3d_resource_governor_generation(governor) == generation);
CHECK(!lardon3d_resource_governor_wait_for_change(
governor,
generation,
1000000ULL
));
/* Test 10 : plusieurs attentes successives fonctionnent. */
for (size_t index = 0; index < 3; ++index) {
CHECK(lardon3d_resource_governor_reserve(
governor,
&snapshot,
&estimate,
&decision,
&reservation
));
CHECK(reservation != NULL);
generation = lardon3d_resource_governor_generation(governor);
CHECK(lardon3d_resource_governor_release(governor, reservation));
CHECK(lardon3d_resource_governor_wait_for_change(
governor,
generation,
5000000000ULL
));
}
lardon3d_resource_governor_destroy(governor);
return true;
}
int
main(void)
{
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
return (run_test() && run_generation_test())
? EXIT_SUCCESS
: EXIT_FAILURE;
}