feat(election-iomgr-refactor): add delay_task_ms() for ms-granularity one-shot tasks

Add millisecond-granularity delay_task_ms() to delaymgr using
clock_gettime(CLOCK_MONOTONIC) for deadline computation. Extends
delay_task_t with nextrun_ts field; delay_worker distinguishes
ms-tasks from second-tasks via non-zero nextrun_ts. Backward
compatible with existing delay_task callers.

4 delay tests pass (2 new: tolerance and invalid-args).
Provides the timeout primitive for election and committing watchdogs.
This commit is contained in:
Rongsong Shen 2026-05-30 07:15:54 +00:00
parent 289c4daaa2
commit 4737b0f8a3
3 changed files with 159 additions and 7 deletions

View file

@ -8,6 +8,22 @@
#include "delay.h"
#include "utils.h"
/* Return 1 if a ms-task's deadline has passed, 0 otherwise. */
static int ms_task_due(const delay_task_t* t)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > t->nextrun_ts.tv_sec) {
return 1;
}
if (now.tv_sec == t->nextrun_ts.tv_sec
&& now.tv_nsec >= t->nextrun_ts.tv_nsec) {
return 1;
}
return 0;
}
static void* delay_worker(void* args)
{
delaymgr_t* mgr = (delaymgr_t*)args;
@ -16,6 +32,7 @@ static void* delay_worker(void* args)
ordq_item_t* it;
delay_task_t* t;
time_t now;
int is_ms_task;
pthread_testcancel();
{
@ -32,10 +49,18 @@ static void* delay_worker(void* args)
continue;
}
now = time(NULL);
t = (delay_task_t*)it->data;
if (t->nextrun > now) {
continue;
t = (delay_task_t*)it->data;
is_ms_task = (t->nextrun_ts.tv_nsec != 0 || t->nextrun_ts.tv_sec != 0);
if (is_ms_task) {
if (!ms_task_due(t)) {
continue;
}
} else {
now = time(NULL);
if (t->nextrun > now) {
continue;
}
}
it = ordq_get(&(mgr->tasks), NULL);
@ -49,10 +74,17 @@ static void* delay_worker(void* args)
if (t->runcount == 0) {
FREEUP(t);
} else {
time_t nextrun = now + t->run_period;
if (is_ms_task) {
/* advance deadline by run_period ms */
long add_ns = (long)t->run_period * 1000000L;
/* put back to taskq as it need to be run again */
t->nextrun = nextrun;
t->nextrun_ts.tv_nsec += add_ns;
t->nextrun_ts.tv_sec += t->nextrun_ts.tv_nsec / 1000000000L;
t->nextrun_ts.tv_nsec %= 1000000000L;
} else {
/* put back to taskq as it need to be run again */
t->nextrun = now + t->run_period;
}
ordq_put(&(mgr->tasks), t);
}
@ -206,3 +238,41 @@ int delay_task(delaymgr_t* mgr, int delay, int runcount,
return ordq_put(&(mgr->tasks), t);
}
int delay_task_ms(delaymgr_t* mgr, int delay_ms, int runcount,
delay_task_entry_fn_t entry, void* args)
{
delay_task_t* t;
struct timespec now;
long add_ns;
if ((mgr == NULL) || (delay_ms <= 0) || (entry == NULL)) {
return -EINVAL;
}
t = (delay_task_t*)calloc(1, sizeof(*t));
if (t == NULL) {
return -ENOMEM;
}
clock_gettime(CLOCK_MONOTONIC, &now);
add_ns = (long)delay_ms * 1000000L;
t->nextrun_ts.tv_sec = now.tv_sec + (now.tv_nsec + add_ns) / 1000000000L;
t->nextrun_ts.tv_nsec = (now.tv_nsec + add_ns) % 1000000000L;
/* Ensure nextrun_ts is never all-zero so the ms-task flag is unambiguous.
* tv_nsec == 0 can only arise when add_ns is an exact multiple of 1s and
* now.tv_nsec == 0; in that edge case bump tv_nsec by 1 ns. */
if (t->nextrun_ts.tv_nsec == 0) {
t->nextrun_ts.tv_nsec = 1;
}
t->nextrun = 0; /* unused for ms-tasks */
t->runcount = runcount;
t->run_period = delay_ms;
t->entry = entry;
t->args = args;
return ordq_put(&(mgr->tasks), t);
}

View file

@ -1,6 +1,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "delay.h"
@ -60,10 +61,89 @@ static void test_delaymgr_precision_ms_field(void)
printf("test_delaymgr_precision_ms_field: PASS\n");
}
/* ---------- RED: test for delay_task_ms (millisecond-granularity) ---------- */
static int ms_work_count = 0;
static int ms_work(void* arg)
{
(void)arg;
ms_work_count++;
return 0;
}
static void test_delay_task_ms_fires_within_tolerance(void)
{
delaymgr_t mgr;
struct timespec t_start, t_end;
long elapsed_ms;
const int delay_ms = 500;
const int tolerance_ms = 200;
const int precision_ms = 50;
ms_work_count = 0;
delaymgr_init(&mgr, precision_ms, NULL);
delaymgr_start(&mgr);
clock_gettime(CLOCK_MONOTONIC, &t_start);
delay_task_ms(&mgr, delay_ms, 1, (delay_task_entry_fn_t)ms_work, NULL);
/* wait long enough: delay + tolerance + one extra precision tick */
{
struct timespec wait = {
.tv_sec = 0,
.tv_nsec = (long)(delay_ms + tolerance_ms + precision_ms) * 1000000L,
};
nanosleep(&wait, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &t_end);
elapsed_ms = (t_end.tv_sec - t_start.tv_sec) * 1000L
+ (t_end.tv_nsec - t_start.tv_nsec) / 1000000L;
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ms_work_count,
"ms task must have fired exactly once");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms >= (delay_ms - tolerance_ms),
"task fired too early");
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms <= (delay_ms + tolerance_ms + precision_ms + 50),
"task fired too late");
delaymgr_fini(&mgr);
printf("test_delay_task_ms_fires_within_tolerance: PASS\n");
}
static void test_delay_task_ms_invalid_args(void)
{
delaymgr_t mgr;
int rc;
delaymgr_init(&mgr, 50, NULL);
/* NULL manager */
rc = delay_task_ms(NULL, 500, 1, (delay_task_entry_fn_t)ms_work, NULL);
TEST_ASSERT_TRUE(rc < 0);
/* zero delay */
rc = delay_task_ms(&mgr, 0, 1, (delay_task_entry_fn_t)ms_work, NULL);
TEST_ASSERT_TRUE(rc < 0);
/* NULL entry */
rc = delay_task_ms(&mgr, 500, 1, NULL, NULL);
TEST_ASSERT_TRUE(rc < 0);
delaymgr_fini(&mgr);
printf("test_delay_task_ms_invalid_args: PASS\n");
}
/* ---------- existing tests still exercise backward compat ---------- */
void test_delay(void)
{
printf("\n>> delay test\n\n");
RUN_TEST(test_delay1);
RUN_TEST(test_delaymgr_precision_ms_field);
RUN_TEST(test_delay_task_ms_fires_within_tolerance);
RUN_TEST(test_delay_task_ms_invalid_args);
}