Skip to content

[daily-ci] FLAKY-TEST: nested prefetch io-threads tests stall out the valgrind integration-type shard (missing valgrind:skip) #48

Description

@madolson

Every Daily valgrind integration-type shard since 2026-09-10 dies on Zset nested prefetch - ZSCORE correctness with pipelined commands hitting the 2400s per-test timeout. The test starts a server with io-threads 4 io-threads-always-active yes (tests/unit/type/zset.tcl:3290), and in that mode idle IO threads busy-spin on pthread_mutex_lock/unlock at src/io_threads.c:423-425 with no syscall and no yield. Valgrind serializes all threads onto one core, so three spinning workers starve the main thread: that server takes 214s just to reach "ok" versus about 3s for every other server in the same run, and 200 sequential ZADDs then made no progress for 41 minutes.

Fix is the valgrind:skip tag, which the io-threads suite already uses for exactly this reason (tests/unit/io-threads.tcl:61 and :142). The three nested-prefetch blocks added by #3940 missed it. Not a production defect: no deployment runs under memcheck.

Details

Jobs

All seven Daily runs 2026-09-10 through 2026-09-16 list test-valgrind-test (integration-type) as failure. Logs read for four:

The twin job test-valgrind-no-malloc-usable-size-test (integration-type) (.github/workflows/daily.yml:945) fails on the identical test in the same runs, verified in 34661154146, 34727724387 and 34792420918. One test currently costs two full valgrind shards per Daily run and masks every tests/unit/type test that would have run after it.

Failing output

Same in all four. Run 34792420918:

=== (zset external:skip) Starting server on 127.0.0.1:21431 ok
[TIMEOUT]: clients state report follows.
sock55850c238830 => (IN PROGRESS) Zset nested prefetch - ZSCORE correctness with pipelined commands

Test Summary: 1200 passed, 1 failed

!!! WARNING The following tests failed:

*** [TIMEOUT]: Zset nested prefetch - ZSCORE correctness with pipelined commands in tests/unit/type/zset.tcl
##[error]Process completed with exit code 1.

No [err], no ==pid== block, no ERROR SUMMARY, no crash anywhere in the four logs. This is not a valgrind report, which is why the automated fingerprinter found no signature. ##[error]Process completed with exit code 1 is runtest exiting non-zero after the timeout.

Failing step is the test step at .github/workflows/daily.yml:884, which runs --single tests/integration --single tests/unit/type with --timeout 2400 (.github/workflows/daily.yml:878-884). timeout-minutes: 1440 at .github/workflows/daily.yml:840 is not the limit; the jobs finished in about 50 minutes.

The test

tests/unit/type/zset.tcl:3290:

start_server {config "minimal.conf" tags {"zset" "external:skip"} overrides {io-threads 4 io-threads-always-active yes zset-max-listpack-entries 0}} {
    test "Zset nested prefetch - ZSCORE correctness with pipelined commands" {
        for {set i 0} {$i < 200} {incr i} {
            r zadd myzset $i "member:$i"
        }

200 sequential ZADDs then 50 pipelined ZSCOREs. Sub-second work natively.

Why it stalls

IOThreadsAfterSleep unlocks every IO thread mutex as soon as an event arrives, in the io_threads_always_active branch at src/io_threads.c:490. Each idle IO thread then spins in IOThreadMain, src/io_threads.c:423-425:

/* If it is locked. We should block until main thread unlocks it. */
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);

With the mutex unlocked both calls return immediately, so the loop at src/io_threads.c:364 spins with no syscall and no yield. waitForClientIO at src/io_threads.c:122-137 spins the same way on the main thread with only atomic_thread_fence between iterations.

Under valgrind all threads are serialized onto one core and preempted on a basic-block quota, so the main thread gets a small slice and every spin iteration costs orders of magnitude more than native. Starvation, not deadlock, but indistinguishable from a hang at this magnitude.

Quantified from run 34792420918's own log. Ordinary servers in that run:

01:38:36.038 === (external:skip) Starting server on 127.0.0.1:21428 ok
01:38:39.374 === (stream needs:debug) Starting server on 127.0.0.1:21429 ok
01:38:42.032 === (zset) Starting server on 127.0.0.1:21430 ok

About 3 seconds each. The io-threads 4 server logs Ready to accept connections tcp at 01:40:18 and the framework does not print ok until 01:43:51: 214 seconds, roughly 70x, before the test body runs at all. The body then logged nothing from 01:48:00 until the framework gave up at 02:29:19.

Introduced by

2d44c74, "Nested prefetching for hash and zset inner hashtables" (#3940), merged 2026-09-09. It added three identical io-threads 4 io-threads-always-active yes start_server blocks with no valgrind:skip:

  • tests/unit/type/hash.tcl:965
  • tests/unit/type/set.tcl:1257
  • tests/unit/type/zset.tcl:3290

The first Daily run after that merge, 34420651673 on 2026-09-10, is the first failure.

Fix

Add "valgrind:skip" to the tags of all three blocks. start_server honours it at tests/support/server.tcl:299. All three need it in one change: only zset is observed failing today because the timeout aborts the run before hash and set are reached, so skipping zset alone just moves the stall.

Ruled out

  • Not a valgrind report. No ==pid==, ERROR SUMMARY or Valgrind error in any log. src/valgrind.sup is irrelevant.
  • Not a build failure. make valgrind SERVER_CFLAGS='-Werror' completed in all four.
  • Not a job-level timeout.
  • Not infra, network or toolchain. Same valgrind 3.22.0-0ubuntu3 on ubuntu-latest, deterministic across seven consecutive runs and two independent job definitions.
  • Not a production defect. The busy-spin is intentional for io-threads-always-active and is only pathological under valgrind's serialized scheduler.

This was generated by AI but verified, with love, by a human.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions