Skip to content

The RSS leak gate's warmup discriminator picks the one batch a per-connection arena step can land in (#765 follow-up) #768

Description

@InauguralPhysicist

What happened

extensions on main failed in run 30439602640 (commit 9caff0c):

FAIL: RSS1 shared_incr leaked 2876 kB over 2000 reqs
      (1472 B/req; threshold 64 kB; first batch 0 kB
       — growth did NOT decay, so this is a leak, not warmup)

Attempt 2 of the same commit passed. The PR branch had passed 40 minutes earlier on the same tree.

It is not a leak

A 24,000-request soak against /sinc grows 0 kB after warmup:

requests 2000 4000 24000
RSS delta +4244 kB 0 0 0

That bounds any real per-request leak at <3 B/req, against the 1472 B/req the gate reported.

At 100-request granularity the warmup is two discrete steps, and the second one moves:

run step 1 step 2
1 req 100, +1576 kB req 400, +2668 kB
2 req 100, +1576 kB req 500, +2668 kB
3 req 100, +1388 kB req 600, +2856 kB
4 req 100, +1388 kB req 700, +2856 kB

Step 2's size (2668–2856 kB) matches CI's reported 2876 kB.

Cause

Every connection is served by a fresh EigsState on its own detached thread (src/ext_http.c:~1400). The first time two workers' lifetimes overlap, glibc allocates a second per-thread malloc arena, and RSS steps by roughly one worker's footprint — once. Arenas are recycled afterwards, which is why RSS is then flat forever.

That overlap is governed by wall-clock scheduling, not request count. On a slower or contended CI runner it lands past request 3000 — i.e. inside measured batch 2, which #765 had just made the sole verdict.

Why #765's rule cannot see this

#765 fixed the front-loaded warmup shape ("one-time warmup decays, a per-request leak does not") by judging the second batch. A late one-shot step has the same "did not decay" signature while being just as one-time.

The discriminating evidence was already in the failure text: first batch 0 kB. A leak of 1472 B/req cannot be absent from the immediately preceding identical batch.

Fix

Three measured batches, verdict = their median. One step, wherever it lands, moves at most one of three; a real leak moves all three. Threshold unchanged at 64 kB.

The verdict rule becomes a pure function of the three numbers, self-tested on every run against ten planted faults (both historical leak rates, the step in each position, a leak with a step on top), plus an end-to-end validation against a real 256 B/req heap leak planted in shared_incr — reported as 272 B/req with all three batches grown.

Note for anyone validating this gate in future: the first fault planted here, dropping a val_decref in shared_incr, leaked nothing measurablemake_num serves request-path Values from the arena, and they die at arena reset regardless of refcount. Validate with something that allocates on the heap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions