#7591·questdb

`FACTORY_TAGS[NATIVE_OFFLOAD] = false` hides regressions of #7267

Author: mdierolfCreated Sep 2, 2026Updated Sep 2, 2026

To reproduce

Note: this bug was discovered and documented by an AI agent that was tasked with diagnosing a memory leak leading to a crash in 9.4.0.

Version: master (checked 9848be5f). Same skip exists in 9.4.0.
Component: tests (AbstractCairoTest, QueryAssertion)
Related: #7267 (factory clear() now resetCapacity()s OFFLOAD lists). Depends on the reduce-ring shrink in #7590 — enabling the tag without that will fail assertMemoryLeak after any JIT filter test.

Description

Per-test native-leak accounting still excludes NATIVE_OFFLOAD:

java
// AbstractCairoTest and QueryAssertion static init
FACTORY_TAGS[MemoryTag.NATIVE_OFFLOAD] = false;

#7267 added AsyncFilterContextTest as the only wiring check, and says so:

java
// NATIVE_OFFLOAD is excluded from the per-factory leak check (AbstractCairoTest),
// so this assertion is the only cover for the wiring.

That means:

  • A revert of AsyncFilterContext.clear()’s resetCapacity() calls would not fail the suite except that one test.
  • A new parallel path that allocates NATIVE_OFFLOAD and forgets to shrink/free it is invisible to assertMemoryLeak.
  • This is how the original 9.4.0 leak survived: an older comment already said OFFLOAD was “ignored for the time being.”

After #7267, a closed factory should hold zero OFFLOAD (lists are Misc.freed in AsyncFilterContext.close()). A cached factory after cursor.close() should hold only initial-capacity lists (256 longs each). Both are assertable.

The remaining reason the global skip exists is the process-lifetime PageFrameReduceTask ring (#7590 ): those lists stay at peak across tests in the same engine.

Suggested fix

  1. Land the PageFrameReduceTask.of() shrink from issue 01 first.

  2. Flip the tag on:

    java
    FACTORY_TAGS[MemoryTag.NATIVE_OFFLOAD] = true;

    in both AbstractCairoTest and QueryAssertion.

  3. Keep AsyncFilterContextTest.testClearShrinksRowIdListsThroughCursorTeardown as the explicit “cursor close while factory lives” guard (that is a capacity assertion, not a zero-bytes assertion).

  4. If some tests still see a bounded residual (initial 256 × workers × 8 bytes on a cached factory that outlives the test’s assertMemoryLeak window), assert NATIVE_OFFLOAD <= workers * initialCapacity * 8 instead of 0 for the cursor-closed-factory-alive case — but engine/factory close must still be 0.

Do not leave the skip in place “because the ring exists.” That is circular: the skip is why the ring leak has no test.

Suggested test (once issue #7590 is in)

java
@Test
public void testPageFrameReduceTaskOfShrinksOffloadOnNewQuery() {
    assertMemoryLeak(() -> {
        PageFrameReduceTask task = new PageFrameReduceTask(configuration, MemoryTag.NATIVE_OFFLOAD);
        try {
            long baseline = Unsafe.getMemUsedByTag(MemoryTag.NATIVE_OFFLOAD);
            task.getFilteredRows().setCapacity(1_000_000);
            Assert.assertTrue(Unsafe.getMemUsedByTag(MemoryTag.NATIVE_OFFLOAD) - baseline >= 1_000_000 * 8);
            // of() with a different sequence id must resetCapacity
            // ... bind a dummy PageFrameSequence with a new id, call of(), then:
            Assert.assertEquals(
                    configuration.getPageFrameReduceRowIdListCapacity(),
                    task.getFilteredRows().getCapacity()
            );
        } finally {
            Misc.free(task);
        }
    });
}

QuestDB version:

master

OS, in case of Docker specify Docker and the Host OS:

Docker, Debian

File System, in case of Docker specify Host File System:

btrfs

Full Name:

Mark Dierolf

Affiliation:

FinancialContent

Have you followed Linux, MacOs kernel configuration steps to increase Maximum open files and Maximum virtual memory areas limit?

  • Yes, I have

Additional context

No response