lardon3d/tests/test_task_checkpoint.c

452 lines
15 KiB
C

#include <fcntl.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <lardon3d/task_checkpoint.h>
#include "resource_snapshot_test_utils.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
enum {
TEST_CHECKPOINT_SIZE = 516,
TEST_CHECKPOINT_HEADER_SIZE = 20,
TEST_MINIMUM_BATCH_OFFSET = 188,
TEST_STARTED_SECONDS_OFFSET = 488,
};
static uint32_t
test_checksum(const unsigned char *data, size_t size)
{
uint32_t hash = UINT32_C(2166136261);
for (size_t index = 0; index < size; ++index) {
hash ^= data[index];
hash *= UINT32_C(16777619);
}
return hash;
}
static void
test_put_u32(unsigned char *output, uint32_t value)
{
for (size_t index = 0; index < 4; ++index) {
output[index] = (unsigned char)(value >> (index * 8));
}
}
static void
test_put_u64(unsigned char *output, uint64_t value)
{
for (size_t index = 0; index < 8; ++index) {
output[index] = (unsigned char)(value >> (index * 8));
}
}
static bool
rewrite_u64(const char *path, size_t offset, uint64_t value)
{
unsigned char data[TEST_CHECKPOINT_SIZE];
int descriptor = open(path, O_RDWR);
if (descriptor < 0
|| read(descriptor, data, sizeof(data)) != (ssize_t)sizeof(data)) {
if (descriptor >= 0) {
(void)close(descriptor);
}
return false;
}
test_put_u64(data + offset, value);
test_put_u32(
data + 16,
test_checksum(
data + TEST_CHECKPOINT_HEADER_SIZE,
TEST_CHECKPOINT_SIZE - TEST_CHECKPOINT_HEADER_SIZE
)
);
bool written = pwrite(descriptor, data, sizeof(data), 0)
== (ssize_t)sizeof(data);
return close(descriptor) == 0 && written;
}
static bool
unused_callback(Lardon3DTask *task, void *userdata)
{
(void)task;
(void)userdata;
return true;
}
typedef struct {
Lardon3DTaskDurableSnapshot snapshot;
bool captured;
} CaptureContext;
static bool
capture_running_callback(Lardon3DTask *task, void *userdata)
{
CaptureContext *context = userdata;
context->captured = lardon3d_task_durable_snapshot(task, &context->snapshot);
return context->captured;
}
typedef struct {
Lardon3DResourceGovernor *governor;
Lardon3DTaskDurableSnapshot snapshot;
bool captured;
} SequenceContext;
static bool
capture_sequence_callback(Lardon3DTask *task, void *userdata)
{
SequenceContext *context = userdata;
Lardon3DResourceReservation *reservation = NULL;
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_sequence_break(
task,
context->governor,
&reservation,
&contract
)) {
return false;
}
context->captured = lardon3d_task_durable_snapshot(task, &context->snapshot);
return context->captured;
}
static Lardon3DTaskDurableSnapshot
snapshot_for(Lardon3DTaskState state)
{
Lardon3DTaskDurableSnapshot snapshot = {
.id = 41,
.estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 8,
.desired_cpu_threads = 1,
.task_class = LARDON3D_RESOURCE_TASK_GENERAL,
},
.progress = state == TASK_COMPLETED ? 100 : 37,
.saved_state = state,
.recovery_state = state == TASK_RUNNING || state == TASK_PAUSED
? TASK_PENDING : state,
.started_at = {.tv_sec = 10, .tv_nsec = 20},
.finished_at = {.tv_sec = 30, .tv_nsec = 40},
.sequence_count = 3,
};
(void)snprintf(snapshot.name, sizeof(snapshot.name), "Tâche durable");
(void)snprintf(snapshot.message, sizeof(snapshot.message), "Frontière validée");
return snapshot;
}
static bool
write_bytes(const char *path, const void *data, size_t size)
{
int descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (descriptor < 0) {
return false;
}
ssize_t written = write(descriptor, data, size);
bool ok = written >= 0 && (size_t)written == size && close(descriptor) == 0;
if (!ok) {
(void)close(descriptor);
}
return ok;
}
static bool
check_restored_state(Lardon3DTaskState saved, Lardon3DTaskState expected)
{
Lardon3DTaskDurableSnapshot durable = snapshot_for(saved);
Lardon3DTask *task = lardon3d_task_restore(
&durable,
unused_callback,
NULL
);
CHECK(task);
Lardon3DTaskSnapshot runtime;
CHECK(lardon3d_task_snapshot(task, &runtime));
CHECK(runtime.state == expected);
CHECK(lardon3d_task_sequence_count(task) == durable.sequence_count);
Lardon3DTaskExecutionContract contract;
CHECK(!lardon3d_task_execution_contract(task, &contract));
lardon3d_task_destroy(task);
return true;
}
static bool
run_test(void)
{
char directory[] = "/tmp/lardon3d-checkpoint-XXXXXX";
CHECK(mkdtemp(directory));
char path[512];
char temporary[512];
char corrupt[512];
char uncertain_directory[512];
char uncertain_path[512];
CHECK(snprintf(path, sizeof(path), "%s/task.chk", directory) > 0);
CHECK(snprintf(temporary, sizeof(temporary), "%s/task.chk.tmp", directory) > 0);
CHECK(snprintf(corrupt, sizeof(corrupt), "%s/corrupt.chk", directory) > 0);
CHECK(snprintf(
uncertain_directory,
sizeof(uncertain_directory),
"%s/no-read",
directory
) > 0);
CHECK(snprintf(
uncertain_path,
sizeof(uncertain_path),
"%s/task.chk",
uncertain_directory
) > 0);
const Lardon3DResourceEstimate estimate = {
.minimum_batch_size = 1,
.maximum_batch_size = 4,
.desired_cpu_threads = 1,
.task_class = LARDON3D_RESOURCE_TASK_GENERAL,
};
Lardon3DTask *pending = lardon3d_task_create(
"Pending",
&estimate,
unused_callback,
NULL
);
CHECK(pending && lardon3d_task_assign_id(pending, 7));
Lardon3DTaskDurableSnapshot durable;
CHECK(lardon3d_task_durable_snapshot(pending, &durable));
CHECK(durable.saved_state == TASK_PENDING);
CHECK(durable.recovery_state == TASK_PENDING);
CHECK(durable.sequence_count == 0);
lardon3d_task_destroy(pending);
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
Lardon3DTaskDurableSnapshot loaded;
uint32_t version = 0;
CHECK(lardon3d_task_checkpoint_load(path, &loaded, &version)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(version == LARDON3D_TASK_CHECKPOINT_VERSION);
CHECK(loaded.id == durable.id && loaded.saved_state == TASK_PENDING);
durable = snapshot_for(TASK_COMPLETED);
durable.id = 99;
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(loaded.id == 99 && loaded.saved_state == TASK_COMPLETED);
/* .next is independently durable and may coexist with a valid canonical
* checkpoint until the Project DB snapshot has committed. */
durable.id = 101;
CHECK(lardon3d_task_checkpoint_stage(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_OK && loaded.id == 99);
CHECK(lardon3d_task_checkpoint_load_staged(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_OK && loaded.id == 101);
CHECK(lardon3d_task_checkpoint_promote_staged(path)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_OK && loaded.id == 101);
CHECK(lardon3d_task_checkpoint_load_staged(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_NOT_FOUND);
CHECK(mkdir(uncertain_directory, 0700) == 0);
Lardon3DTaskDurableSnapshot previous = durable;
previous.id = 98;
CHECK(lardon3d_task_checkpoint_save(uncertain_path, &previous)
== LARDON3D_TASK_CHECKPOINT_OK);
durable.id = 100;
CHECK(setenv(
"LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE",
"1",
1
) == 0);
Lardon3DTaskCheckpointResult uncertain = lardon3d_task_checkpoint_save(
uncertain_path,
&durable
);
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE") == 0);
CHECK(uncertain == LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE);
CHECK(lardon3d_task_checkpoint_load(uncertain_path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(loaded.id == 100);
unsigned char short_data[12] = {'L', '3', 'D', 'T', 'A', 'S', 'K', 0};
CHECK(write_bytes(corrupt, short_data, sizeof(short_data)));
CHECK(lardon3d_task_checkpoint_load(corrupt, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_INVALID);
int descriptor = open(path, O_RDWR);
CHECK(descriptor >= 0);
unsigned char unknown_version[4] = {2, 0, 0, 0};
CHECK(pwrite(descriptor, unknown_version, sizeof(unknown_version), 8)
== (ssize_t)sizeof(unknown_version));
CHECK(close(descriptor) == 0);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, &version)
== LARDON3D_TASK_CHECKPOINT_UNSUPPORTED_VERSION);
CHECK(version == 2);
durable = snapshot_for(TASK_PENDING);
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
descriptor = open(path, O_RDWR);
CHECK(descriptor >= 0);
unsigned char changed = 'X';
CHECK(pwrite(descriptor, &changed, 1, 100) == 1);
CHECK(close(descriptor) == 0);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_INVALID);
durable = snapshot_for(TASK_PENDING);
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(rewrite_u64(path, TEST_STARTED_SECONDS_OFFSET, UINT64_MAX));
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_INVALID);
if (sizeof(size_t) < sizeof(uint64_t)) {
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_OK);
CHECK(rewrite_u64(path, TEST_MINIMUM_BATCH_OFFSET, UINT64_MAX));
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_INVALID);
}
const unsigned char incomplete[] = "incomplet";
CHECK(write_bytes(temporary, incomplete, sizeof(incomplete)));
CHECK(unlink(path) == 0);
CHECK(lardon3d_task_checkpoint_load(path, &loaded, NULL)
== LARDON3D_TASK_CHECKPOINT_NOT_FOUND);
durable = snapshot_for(TASK_PENDING);
durable.estimate.maximum_batch_size = 0;
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_INVALID);
durable = snapshot_for(TASK_PENDING);
memset(durable.name, 'x', sizeof(durable.name));
CHECK(lardon3d_task_checkpoint_save(path, &durable)
== LARDON3D_TASK_CHECKPOINT_INVALID);
CHECK(check_restored_state(TASK_COMPLETED, TASK_COMPLETED));
CHECK(check_restored_state(TASK_FAILED, TASK_FAILED));
CHECK(check_restored_state(TASK_CANCELLED, TASK_CANCELLED));
CHECK(check_restored_state(TASK_RUNNING, TASK_PENDING));
CHECK(check_restored_state(TASK_PAUSED, TASK_PENDING));
Lardon3DHardwareProfile profile = {
.logical_cpu_count = UINT_MAX,
.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
);
Lardon3DResourceSnapshot resources = {
.memory_available_bytes = UINT64_MAX,
.cpu_load_1m = 0.0,
};
Lardon3DResourceDecision decision;
Lardon3DResourceReservation *reservation = NULL;
CaptureContext running = {0};
Lardon3DTask *running_task = lardon3d_task_create(
"Running",
&estimate,
capture_running_callback,
&running
);
CHECK(governor && running_task && lardon3d_task_assign_id(running_task, 51));
CHECK(lardon3d_test_resource_snapshot_make_fresh(&resources));
CHECK(lardon3d_resource_governor_reserve(
governor, &resources, &estimate, &decision, &reservation
));
CHECK(lardon3d_task_start(running_task, governor, reservation));
CHECK(running.captured && running.snapshot.saved_state == TASK_RUNNING);
CHECK(running.snapshot.recovery_state == TASK_PENDING);
lardon3d_task_destroy(running_task);
Lardon3DTaskDurableSnapshot restarted_snapshot = snapshot_for(TASK_RUNNING);
restarted_snapshot.started_at = (struct timespec) {.tv_sec = 10, .tv_nsec = 20};
restarted_snapshot.finished_at = (struct timespec) {.tv_sec = 30, .tv_nsec = 40};
CaptureContext restarted = {0};
Lardon3DTask *restarted_task = lardon3d_task_restore(
&restarted_snapshot,
capture_running_callback,
&restarted
);
CHECK(restarted_task);
reservation = NULL;
CHECK(lardon3d_test_resource_snapshot_make_fresh(&resources));
CHECK(lardon3d_resource_governor_reserve(
governor, &resources, &estimate, &decision, &reservation
));
CHECK(lardon3d_task_start(restarted_task, governor, reservation));
CHECK(restarted.captured);
CHECK(restarted.snapshot.started_at.tv_sec != 10);
CHECK(restarted.snapshot.finished_at.tv_sec == 0);
CHECK(restarted.snapshot.finished_at.tv_nsec == 0);
lardon3d_task_destroy(restarted_task);
SequenceContext sequence = {.governor = governor};
Lardon3DTask *sequence_task = lardon3d_task_create(
"Sequence",
&estimate,
capture_sequence_callback,
&sequence
);
CHECK(sequence_task && lardon3d_task_assign_id(sequence_task, 52));
reservation = NULL;
CHECK(lardon3d_test_resource_snapshot_make_fresh(&resources));
CHECK(lardon3d_resource_governor_reserve(
governor, &resources, &estimate, &decision, &reservation
));
CHECK(lardon3d_task_start(sequence_task, governor, reservation));
CHECK(sequence.captured && sequence.snapshot.saved_state == TASK_RUNNING);
CHECK(sequence.snapshot.recovery_state == TASK_PENDING);
CHECK(sequence.snapshot.sequence_count == 1);
Lardon3DTask *restored_sequence = lardon3d_task_restore(
&sequence.snapshot,
unused_callback,
NULL
);
CHECK(restored_sequence);
CHECK(lardon3d_task_sequence_count(restored_sequence) == 1);
Lardon3DTaskExecutionContract restored_contract;
CHECK(!lardon3d_task_execution_contract(
restored_sequence,
&restored_contract
));
lardon3d_task_destroy(restored_sequence);
lardon3d_task_destroy(sequence_task);
lardon3d_resource_governor_destroy(governor);
CHECK(unlink(temporary) == 0);
CHECK(unlink(corrupt) == 0);
CHECK(unlink(uncertain_path) == 0);
CHECK(rmdir(uncertain_directory) == 0);
CHECK(rmdir(directory) == 0);
return true;
}
int
main(void)
{
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
}