From 4737b0f8a3f2b10cf7580151148fd5ef8322b4f4 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sat, 30 May 2026 07:15:54 +0000 Subject: [PATCH] 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. --- src/include/delay.h | 2 + src/lib/delay.c | 84 +++++++++++++++++++++++++++++++--- src/lib/testsuite/test_delay.c | 80 ++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 7 deletions(-) diff --git a/src/include/delay.h b/src/include/delay.h index 19a306d8..ed02046c 100644 --- a/src/include/delay.h +++ b/src/include/delay.h @@ -14,6 +14,7 @@ typedef int (*task_run_fn_t)(delay_task_entry_fn_t, void*); typedef struct { time_t nextrun; + struct timespec nextrun_ts; /* non-zero tv_nsec flags an ms-task */ int run_period; int runcount; delay_task_entry_fn_t entry; @@ -34,5 +35,6 @@ extern void delaymgr_stop(delaymgr_t*); extern void delaymgr_fini(delaymgr_t*); extern int delaymgr_start(delaymgr_t*); extern int delay_task(delaymgr_t*, int, int, delay_task_entry_fn_t, void*); +extern int delay_task_ms(delaymgr_t*, int, int, delay_task_entry_fn_t, void*); #endif diff --git a/src/lib/delay.c b/src/lib/delay.c index ca1ab786..001fb690 100644 --- a/src/lib/delay.c +++ b/src/lib/delay.c @@ -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); +} diff --git a/src/lib/testsuite/test_delay.c b/src/lib/testsuite/test_delay.c index 96acd359..47717fd0 100644 --- a/src/lib/testsuite/test_delay.c +++ b/src/lib/testsuite/test_delay.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #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); }