Stress Scenarios
Stress scenarios are parameterized test programs that subject an extension function to adversarial conditions that rarely appear in normal unit tests. Run them with ext_memcheck.run_scenario(). Refer to the API documentation for usage details.
growth_benchmark
Section titled “growth_benchmark”Invokes the target SQL a configurable number of iterations and tracks the per-MemoryContext used bytes at log-spaced checkpoints (iteration 1, 10, 100, …, up to a maximum of 8 checkpoints). Contexts that grow monotonically across at least two checkpoints and exceed pg_ext_memcheck.bloat_min_bytes (default 8 KiB) are reported as ctx_bloat violations.
SELECT ext_memcheck.run_scenario(scenario_name := 'growth_benchmark', iterations := 1000, workload := 'SELECT 1');How it works:
- A private
AllocSetcontext (pg_ext_memcheck bench) is created to own the bookkeeping data — this context is excluded from analysis. - Checkpoint iteration numbers are computed as
1, 10, 100, …(powers of 10, capped atiterations). Up to 8 checkpoints are used. - After each checkpoint iteration,
context_walkersnapshots the fullMemoryContexttree and records used bytes (allocated − freed) for every context, identified by (name, depth, parent hash). - Contexts that appear mid-run are back-filled with
0; contexts that disappear are carried forward at their last observed value. - After all iterations complete, each tracked context is analyzed:
- Must have grown from first to last checkpoint.
- Must show monotonic (non-decreasing) growth.
- Must have increased at 2+ checkpoints (single-shot spikes are ignored).
- Total growth must be ≥
bloat_min_bytes.
- Growth shape is classified as superlinear when the per-iteration rate in the last checkpoint interval exceeds 1.5× the rate in the first interval; otherwise linear.
- Violations are written to the ring buffer as
ctx_bloat. The generic before/after diff is skipped for this scenario to avoid double-reporting the same growth ascontext_leak.
Severity:
| Level | Base condition | Escalation |
|---|---|---|
ERROR | Total growth > 1 MiB | or WARNING + superlinear growth |
WARNING | Total growth > 64 KiB | or INFO + superlinear growth |
INFO | Total growth ≥ bloat_min_bytes | — |
What it catches: Slow cumulative leaks that are invisible in single-call tests; monotonic context bloat; accelerating (superlinear) allocation patterns that will exhaust memory under sustained load.
Tuning:
-- Raise sensitivity: report any context that grew by at least 4 KiBSET pg_ext_memcheck.bloat_min_bytes = 4096;
-- Run 1000 iterations; checkpoints will be at 1, 10, 100, 1000SELECT ext_memcheck.run_scenario('growth_benchmark', 1000, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type = 'ctx_bloat';tx_abort_loop
Section titled “tx_abort_loop”Invokes the target function inside a savepoint, then rolls back to the savepoint before releasing it. Repeats many times. Exposes resources that are only released on COMMIT, not ROLLBACK.
SELECT ext_memcheck.run_scenario(scenario_name := 'tx_abort_loop', iterations := 100, workload := 'SELECT 1');How it works: Similar to growth_benchmark, but wraps each invocation in a SAVEPOINT / ROLLBACK TO SAVEPOINT block. The context walker compares pre/post snapshots across the entire loop, so any resources that accumulate across iterations without being cleaned up on rollback will show up as a leak.
What it catches: Context leaks that only manifest on transaction abort; resources tied to transaction callbacks that are never called on abort.
shmem_sentinel_probe
Section titled “shmem_sentinel_probe”Plants 0xDE sentinel bytes just past the declared boundary of each registered shared memory segment, runs the workload, then verifies all sentinels are intact. A corrupted sentinel means the extension under test wrote past its declared shmem boundary.
SELECT ext_memcheck.run_scenario('shmem_sentinel_probe', 10, 'SELECT 1');
-- Inspect any overrun violationsSELECT * FROM ext_memcheck.violation_log WHERE check_type = 'shmem_overrun';
-- Reset registry between test runsSELECT ext_memcheck.clear_shmem_registry();How it works: shmem_probe.c allocates a ProbeRegistry in shared memory. The scenario calls probe_register(seg_name, alloc_size, data_end) for the ViolationLog and DsmTrackerState segments — both allocated with an extra byte in _PG_init, so alloc_size = sizeof(struct) + 1 and the sentinel is placed at data_end = sizeof(struct). After the workload loop, probe_check_all() reads each sentinel byte. Any value other than 0xDE logs a shmem_overrun ERROR violation.
To probe your own extension’s shared memory segment, call ext_memcheck.register_shmem_probe(seg_name, allocated_size) before running this scenario.
What it catches: Off-by-one writes past a segment’s declared boundary; memset or memcpy calls whose length is computed incorrectly.
wrong_context_probe
Section titled “wrong_context_probe”Runs the target workload SQL a configurable number of times, then diffs the MemoryContext tree before and after to detect allocations that landed in long-lived global contexts (TopMemoryContext, CacheMemoryContext). Unlike the generic executor-hook path, this scenario skips the context_leak diff entirely — it runs only check_wrong_context_alloc, so results are focused exclusively on wrong-context violations without noise from ordinary leak detection.
SELECT ext_memcheck.run_scenario('wrong_context_probe', 50, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type = 'wrong_ctx_alloc';How it works:
- Before the first iteration,
context_walkersnapshots the fullMemoryContexttree. - The workload SQL is executed
iterationstimes via SPI. - After the last iteration, a second snapshot is taken and
check_wrong_context_allocis called with the before/after pair. check_wrong_context_allocruns two passes:- Growth pass — any named global context (
TopMemoryContext,CacheMemoryContext) whosetotalAllocatedincreased since the pre-run snapshot emits awrong_ctx_allocWARNING with the byte delta. - New-child pass — any context that appeared in the post-run snapshot but not the pre-run snapshot and whose parent is a known global emits a
wrong_ctx_allocWARNING identifying the newly created child.
- Growth pass — any named global context (
What it catches: Extension functions that allocate in long-lived contexts (TopMemoryContext, CacheMemoryContext) instead of query-local contexts — a common source of per-backend memory growth that accumulates silently across sessions.
When to use over the default executor hook: The executor hook also calls check_wrong_context_alloc, but it does so alongside context-leak detection and only for queries passing through the hook. Use wrong_context_probe when you want a clean, isolated signal across N controlled repetitions without unrelated leak noise.
use_after_reset
Section titled “use_after_reset”Runs the use_after_reset crash scenario inside a BGWorker process. The worker calls elog(FATAL) to simulate a use-after-reset crash, then exits with a non-zero exit code. The calling backend detects the crash via WorkerSlot.exit_code and logs the result.
SELECT ext_memcheck.run_scenario('use_after_reset', 1, 'SELECT 1');SELECT ext_memcheck.flush_violations();How it works:
launch_crash_isolation_worker("use_after_reset")fills a shared-memoryWorkerSlotwith the scenario name and current database.- A
BackgroundWorkeris registered and launched. The caller blocks onWaitForBackgroundWorkerShutdown(). - The worker calls
run_use_after_reset_in_worker(), which callselog(FATAL)— a cleanproc_exit(1)that does not trigger postmaster crash recovery. - The calling backend reads
exit_code != 0from the slot and reports the confirmed crash.
What it catches: Verifies that a known use-after-reset bug produces a crash signal without terminating the test session. Used to validate that crash-isolation infrastructure works correctly.
oom_simulation
Section titled “oom_simulation”Allocates 1 MiB chunks via palloc_extended(MCXT_ALLOC_NO_OOM) inside a BGWorker until the allocator returns NULL (or 256 MiB are consumed), then exits with elog(FATAL). The calling backend detects the crash via exit code.
SELECT ext_memcheck.run_scenario('oom_simulation', 1, 'SELECT 1');SELECT ext_memcheck.flush_violations();How it works: Mirrors use_after_reset but exercises the OOM allocation path. MCXT_ALLOC_NO_OOM suppresses the normal ERROR and returns NULL instead, so the loop can drain available memory to a controlled limit (capped at 256 MB for CI environments with Linux memory overcommit).
What it catches: Confirms that an extension’s OOM behavior (or simulated OOM) is crash-detected in isolation without affecting the test session or other backends.
context_reset_storm
Section titled “context_reset_storm”Allocates 100 × 64-byte blocks into a private scratch MemoryContext, calls MemoryContextReset() to invalidate every pointer into it, then runs the workload via SPI — repeated iterations times. Reveals extensions that cache raw pointers into contexts they do not own.
SELECT ext_memcheck.run_scenario('context_reset_storm', 50, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type IN ('context_leak', 'wrong_ctx_alloc');How it works:
- A scratch
AllocSetcontext namedpg_ext_memcheck stormis created underTopMemoryContext. - For each iteration:
- 100 × 64-byte blocks are allocated into the scratch context, simulating extension state.
MemoryContextReset()is called — all pointers into the context are now stale.- The workload SQL executes via SPI. If the extension dereferences a pointer into the now-reset context, behaviour is undefined.
- After all iterations, the before/after context diff catches any retained allocations.
- The scratch context is deleted on clean exit.
What it catches: Extensions that hold raw pointers into short-lived contexts across call boundaries; state cached in query-local storage that is accessed in a later query after its owning context has been reset.
Note: For extensions that are known to crash on use-after-reset, redirect testing to the use_after_reset scenario, which runs in a crash-isolated BGWorker so a SIGSEGV does not terminate the test session.
cursor_leak
Section titled “cursor_leak”Opens up to 32 SPI cursors (portals) without closing them, runs the workload iterations times while all remain open, then lets SPI_finish() drain and close them. Reveals portal-context accumulation during the open window.
SELECT ext_memcheck.run_scenario('cursor_leak', 10, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type = 'context_leak';How it works:
min(iterations, 32)cursors are opened viaSPI_cursor_open_with_args()and left open.- The workload SQL executes
iterationstimes while all cursors remain open, simulating concurrent portal usage alongside the extension under test. - The before/after context diff (taken in
run_scenario) captures anyPortalContextchild growth during this window ascontext_leakviolations. SPI_finish()closes all open cursors, leaving the backend clean.
What it catches: Extensions that open internal cursors or portals without closing them; portal context accumulation from SPI queries that are never finished; DSM segment leaks tied to cursor lifetime.
cold_warm_cold
Section titled “cold_warm_cold”Runs the workload for a cold batch, sleeps for 1 second to simulate an idle period, then runs a warm batch. The before/after context diff across the whole scenario reveals CacheMemoryContext or extension-cache growth that persists across idle boundaries.
SELECT ext_memcheck.run_scenario('cold_warm_cold', 6, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type IN ('context_leak', 'wrong_ctx_alloc');How it works:
floor(iterations / 3)workload executions run as the cold phase — extension is warming up its caches.SELECT pg_sleep(1)via SPI simulates the backend sitting idle (e.g., between connection pool checkout events).- Another
floor(iterations / 3)executions run as the warm phase — extension uses its now-warmed state. - The before/after context diff across the entire scenario shows whether any context grew from the start of cold to the end of warm — indicative of a cache that accumulates without eviction.
| Phase | Iterations | Purpose |
|---|---|---|
| Cold | floor(iterations / 3) | Prime extension caches |
| Idle | 1 s sleep | Simulate inactivity between uses |
| Warm | floor(iterations / 3) | Exercise with warm state |
What it catches: CacheMemoryContext growth that accumulates across idle periods; extension-level caches that grow monotonically without any eviction policy; allocations into long-lived contexts that only appear after the extension has been used at least once.
Note: The 1-second sleep is intentional. Set iterations to at least 6 (giving ≥ 2 iterations per phase) for meaningful results.
concurrent_backends
Section titled “concurrent_backends”Launches up to 16 sequential BGWorker backends — each running the workload in a fresh backend process — and detects any crash or error across those launches. Each worker connects to the same database, executes the workload, and exits cleanly or with a non-zero code that is recorded as a concurrent_backends ERROR violation.
SELECT ext_memcheck.run_scenario('concurrent_backends', 5, 'SELECT your_ext.fn()');SELECT ext_memcheck.flush_violations();SELECT * FROM ext_memcheck.violation_log WHERE check_type = 'concurrent_backends';How it works:
min(iterations, 16)workers are launched, one at a time, vialaunch_workload_worker().- Each worker connects to the current database, executes the workload SQL via SPI in a transaction, and exits.
- The calling backend blocks on
WaitForBackgroundWorkerShutdown()between launches (sequential, not truly concurrent — see note below). - Any worker that exits with a non-zero code (ERROR, FATAL, SIGSEGV) logs a
concurrent_backendsERROR violation to the shared ring buffer.
What it catches: Crashes that only appear in a fresh backend process (e.g., missing shmem re-attach on reconnect); extension code that assumes single-backend execution; shmem access patterns that corrupt data when exercised across multiple reconnects.
dsm_lifecycle_check (planned)
Section titled “dsm_lifecycle_check (planned)”Runs the target function inside a DSM segment allocation/deallocation cycle and verifies that the extension correctly attaches and detaches.
What it catches: Leaked DSM segment handles; missing dsm_detach() calls on backend exit.