The result read's losing side of the seqlock is driven by a hook inside the window between the copy and the check
This commit is contained in:
parent
bf827dc55b
commit
c2f26a9909
11
TODO.org
11
TODO.org
@ -1336,10 +1336,13 @@ pops the live restart list, and the generation stamp keeps a nested break from
|
|||||||
claiming a choice made against the outer one. Rules out deleting them with the
|
claiming a choice made against the outer one. Rules out deleting them with the
|
||||||
transport.
|
transport.
|
||||||
|
|
||||||
** TODO The seqlock's losing race has no test
|
** DONE The seqlock's losing race has no test
|
||||||
The result read copies into the caller's buffer and checks the counter either
|
CLOSED: [2026-09-25]
|
||||||
side; nothing drives the case where the counter moves. The one threaded test races
|
=flan_dev_result_read_hook= runs between the copy and the second counter read,
|
||||||
the registry table instead.
|
and dev_limits.c's =race= mode writes from inside that window: once (the read
|
||||||
|
retries and returns the new value), every attempt (it gives up with nothing), and
|
||||||
|
a write left open (it never copies). A hook rather than a second thread, so the
|
||||||
|
interleaving is the same on every run; rules out a timing-based stress test here.
|
||||||
|
|
||||||
** TODO The snapshot generation's racing stale claim has no test
|
** TODO The snapshot generation's racing stale claim has no test
|
||||||
The nested-break case is tested deterministically now. What is still absent is the
|
The nested-break case is tested deterministically now. What is still absent is the
|
||||||
|
|||||||
@ -429,6 +429,13 @@ void flan_dev_result_end(void) {
|
|||||||
* when it was sizing something to send through a socket. */
|
* when it was sizing something to send through a socket. */
|
||||||
uint64_t flan_dev_result_cap(void) { return RESULT_MAX; }
|
uint64_t flan_dev_result_cap(void) { return RESULT_MAX; }
|
||||||
|
|
||||||
|
/* Called between the copy and the second read of the counter, when set. It
|
||||||
|
* exists for test/dev_limits.c and nothing else sets it: the losing side of
|
||||||
|
* the race is a write landing inside that window, and a second thread cannot
|
||||||
|
* be made to land there on demand. A hook that writes a value from inside the
|
||||||
|
* window is the same interleaving, every time. */
|
||||||
|
void (*flan_dev_result_read_hook)(void);
|
||||||
|
|
||||||
int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen,
|
int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen,
|
||||||
uint64_t *len) {
|
uint64_t *len) {
|
||||||
for (int attempt = 0; attempt < 64; attempt++) {
|
for (int attempt = 0; attempt < 64; attempt++) {
|
||||||
@ -438,6 +445,7 @@ int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen,
|
|||||||
if (n > RESULT_MAX) n = RESULT_MAX; /* a torn read cannot overrun */
|
if (n > RESULT_MAX) n = RESULT_MAX; /* a torn read cannot overrun */
|
||||||
if ((uint64_t)n > cap) n = (size_t)cap;
|
if ((uint64_t)n > cap) n = (size_t)cap;
|
||||||
memcpy(dst, result, n);
|
memcpy(dst, result, n);
|
||||||
|
if (flan_dev_result_read_hook != NULL) flan_dev_result_read_hook();
|
||||||
/* The copy must be ordered before the second read of the counter, or the
|
/* The copy must be ordered before the second read of the counter, or the
|
||||||
* check is of a copy the compiler was free to make afterwards. */
|
* check is of a copy the compiler was free to make afterwards. */
|
||||||
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
||||||
|
|||||||
@ -363,15 +363,91 @@ static int regrace(void) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── The seqlock's losing side ───────────────────────────────────────
|
||||||
|
*
|
||||||
|
* [flan_dev_result_read] copies, then checks the counter has not moved. The
|
||||||
|
* single-threaded reads above only ever take the winning side. These take the
|
||||||
|
* other one, deterministically: [flan_dev_result_read_hook] runs inside the
|
||||||
|
* window between the copy and the check, and what it does there is a write.
|
||||||
|
*
|
||||||
|
* Three writers. One that publishes a value once: the read must throw its
|
||||||
|
* copy away and come back with the new value, numbered one higher — a read
|
||||||
|
* that did not check would return the old bytes under the old number, which
|
||||||
|
* is a stale answer described as current. One that publishes on every
|
||||||
|
* attempt: the read runs out of attempts and says so, with no bytes and the
|
||||||
|
* number of the last complete value. And one that opens a write and never
|
||||||
|
* closes it: every attempt sees an odd counter, so the copy never happens and
|
||||||
|
* the hook never runs. */
|
||||||
|
extern void (*flan_dev_result_read_hook)(void);
|
||||||
|
|
||||||
|
static int race_calls;
|
||||||
|
|
||||||
|
static void publish(const char *v) {
|
||||||
|
flan_dev_result_begin();
|
||||||
|
flan_dev_emit((const uint8_t *)v, (int64_t)strlen(v));
|
||||||
|
flan_dev_result_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void race_once(void) {
|
||||||
|
race_calls++;
|
||||||
|
flan_dev_result_read_hook = NULL;
|
||||||
|
publish("second");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void race_every(void) {
|
||||||
|
race_calls++;
|
||||||
|
publish("again");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int race(void) {
|
||||||
|
uint64_t gen0, gen, len;
|
||||||
|
int ok;
|
||||||
|
|
||||||
|
publish("first");
|
||||||
|
(void)result_read(&gen0, &len);
|
||||||
|
|
||||||
|
race_calls = 0;
|
||||||
|
flan_dev_result_read_hook = race_once;
|
||||||
|
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
|
||||||
|
printf("once ok %d calls %d gen +%llu value %.*s\n", ok, race_calls,
|
||||||
|
(unsigned long long)(gen - gen0), (int)len, rbuf);
|
||||||
|
|
||||||
|
race_calls = 0;
|
||||||
|
flan_dev_result_read_hook = race_every;
|
||||||
|
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
|
||||||
|
flan_dev_result_read_hook = NULL;
|
||||||
|
{
|
||||||
|
uint64_t now, nlen;
|
||||||
|
(void)result_read(&now, &nlen);
|
||||||
|
printf("every ok %d calls %d len %llu gen %s\n", ok, race_calls,
|
||||||
|
(unsigned long long)len,
|
||||||
|
gen == now ? "last complete" : "not the last complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
race_calls = 0;
|
||||||
|
(void)result_read(&gen0, &len);
|
||||||
|
flan_dev_result_read_hook = race_every;
|
||||||
|
flan_dev_result_begin();
|
||||||
|
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
|
||||||
|
flan_dev_result_read_hook = NULL;
|
||||||
|
printf("open ok %d calls %d len %llu gen +%llu\n", ok, race_calls,
|
||||||
|
(unsigned long long)len, (unsigned long long)(gen - gen0));
|
||||||
|
flan_dev_result_end();
|
||||||
|
(void)result_read(&gen, &len);
|
||||||
|
printf("closed gen +%llu\n", (unsigned long long)(gen - gen0));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
flan_rt_init(argc, argv);
|
flan_rt_init(argc, argv);
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s cap|names|regfull|regchurn|regrace|regoverflow\n",
|
"usage: %s cap|race|names|regfull|regchurn|regrace|regoverflow\n",
|
||||||
argv[0]);
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (strcmp(argv[1], "cap") == 0) return cap();
|
if (strcmp(argv[1], "cap") == 0) return cap();
|
||||||
|
if (strcmp(argv[1], "race") == 0) return race();
|
||||||
if (strcmp(argv[1], "names") == 0) return names();
|
if (strcmp(argv[1], "names") == 0) return names();
|
||||||
if (strcmp(argv[1], "regfull") == 0) return regfull();
|
if (strcmp(argv[1], "regfull") == 0) return regfull();
|
||||||
if (strcmp(argv[1], "regchurn") == 0) return regchurn();
|
if (strcmp(argv[1], "regchurn") == 0) return regchurn();
|
||||||
|
|||||||
@ -547,6 +547,22 @@ let () =
|
|||||||
if code <> 0 || out <> want_cap then
|
if code <> 0 || out <> want_cap then
|
||||||
fail "the 4K result cap\n got: %S (exit %d)\n wanted: %S"
|
fail "the 4K result cap\n got: %S (exit %d)\n wanted: %S"
|
||||||
out code want_cap;
|
out code want_cap;
|
||||||
|
(* The seqlock's losing side, driven by a hook inside the window between
|
||||||
|
the copy and the check. A write landing there once makes the read come
|
||||||
|
back with the new value, numbered one higher, on its second attempt. A
|
||||||
|
write landing there every time exhausts the 64 attempts and answers
|
||||||
|
nothing, under the number of the last complete value. A write left open
|
||||||
|
is refused before any copy, so the hook never runs. *)
|
||||||
|
let code, out, err = mode "race" in
|
||||||
|
let want_race =
|
||||||
|
"once ok 1 calls 1 gen +1 value second\n\
|
||||||
|
every ok 0 calls 64 len 0 gen last complete\n\
|
||||||
|
open ok 0 calls 0 len 0 gen +0\n\
|
||||||
|
closed gen +1\n"
|
||||||
|
in
|
||||||
|
if code <> 0 || out <> want_race then
|
||||||
|
fail "the result read losing its race\n got: %S (exit %d, err %S)\n wanted: %S"
|
||||||
|
out code err want_race;
|
||||||
(* 4096 distinct names fit; the next one stops the process. The table is
|
(* 4096 distinct names fit; the next one stops the process. The table is
|
||||||
fixed and never moves, because a loaded module holds the address of a
|
fixed and never moves, because a loaded module holds the address of a
|
||||||
cell in it, so growing is not available and overrunning is the only
|
cell in it, so growing is not available and overrunning is the only
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user