diff --git a/src/controller/config_repl.c b/src/controller/config_repl.c index 626edba..d3a5944 100644 --- a/src/controller/config_repl.c +++ b/src/controller/config_repl.c @@ -1011,16 +1011,24 @@ int conf_check_stale(const char *workdir, time_t stale_timeout_s) /* ── conf_trim_old_versions ─────────────────────── */ typedef struct { - char name[NAME_MAX]; - time_t mtime; + char name[NAME_MAX]; + struct timespec mtime; /* st_mtim — nanosecond resolution. Trimming must + * not tie on second-granularity st_mtime: configs + * saved within the same second (rapid replication + * or unit tests) would sort as equal and leave the + * protected current slot as the only trim + * candidate — which the pointer-protection skips, + * so nothing ever gets pruned. */ } ver_entry_t; static int ver_entry_cmp_desc(const void *a, const void *b) { const ver_entry_t *ea = (const ver_entry_t *)a; const ver_entry_t *eb = (const ver_entry_t *)b; - if (eb->mtime > ea->mtime) return 1; - if (eb->mtime < ea->mtime) return -1; + if (eb->mtime.tv_sec != ea->mtime.tv_sec) + return (eb->mtime.tv_sec > ea->mtime.tv_sec) ? 1 : -1; + if (eb->mtime.tv_nsec != ea->mtime.tv_nsec) + return (eb->mtime.tv_nsec > ea->mtime.tv_nsec) ? 1 : -1; return 0; } @@ -1059,7 +1067,7 @@ void conf_trim_old_versions(const char *workdir, int keep_n) if (stat(full, &st) == 0) { strncpy(entries[n].name, e->d_name, NAME_MAX - 1); entries[n].name[NAME_MAX - 1] = '\0'; - entries[n].mtime = st.st_mtime; + entries[n].mtime = st.st_mtim; n++; } } diff --git a/src/controller/testsuite/test_config_repl.c b/src/controller/testsuite/test_config_repl.c index 5c40758..a0336c0 100644 --- a/src/controller/testsuite/test_config_repl.c +++ b/src/controller/testsuite/test_config_repl.c @@ -61,8 +61,16 @@ static char tmpdir[256]; static void cr_setup(void) { - snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_cr_%d_%ld", - (int)getpid(), (long)time(NULL)); + static unsigned int cr_seq; + /* Unique per invocation, not just per (pid, second): the 1000+ tests in + * this suite routinely run within the same wall-clock second, and a + * Unity assertion failure longjmps out of the test, skipping the + * cr_teardown() that would rm -rf the dir — so a reused path would make + * the next test silently inherit the previous one's cluster.bin.* files + * and conf/current (observed as a cascade: a leaked trim-test dir made + * test_conf_get_saved_tag_no_config read a real tag instead of 0). */ + snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_cr_%d_%u_%ld", + (int)getpid(), cr_seq++, (long)time(NULL)); mkdir(g_tmpdir, 0700); /* conf_save_config() writes to workdir/name/conf — mirror that here */ snprintf(g_instdir, sizeof(g_instdir), "%s/ctrl0", g_tmpdir);