LoadTarget.java
package io.github.baokhang83.mnemo.warden.agent.harness;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
* W-206 test fixture, not a production entry point: a real target JVM that retains a
* controllable amount of live heap, so {@code deploy/verify-oomkill-safety.sh} can deterministically
* pose two scenarios — adversarial load that must survive as a correctly aborted shrink, and
* a genuinely idle heap that must survive as a completed one. A live-held allocation is used
* instead of driving real HTTP traffic against it: traffic-driven garbage would mostly be
* collected by {@code ShrinkSequence}'s own deep GC step regardless of "load," so it can't
* reliably force the RSS gate either way — a fixed live set can.
*
* <p>Runs under the same JMX flags and {@code -XX:+UseG1GC} as {@code example-sidecar.yaml}'s
* {@code app} container (see {@code TargetAttacher}'s javadoc for why those flags are load-bearing),
* launched from the same image as a different main class ({@code java -cp warden-agent.jar
* ...harness.LoadTarget}), so the safety check needs no separate image build.
*/
public final class LoadTarget {
/** How many megabytes of live heap to retain; 0 (default) retains none. */
public static final String ENV_RETAIN_MB = "WARDEN_LOAD_TARGET_RETAIN_MB";
// Deliberately well under 1 MiB, not a round 1 MiB chunk: G1's default region size can be as
// small as 1 MiB, and any object over half a region becomes a "humongous" allocation spanning
// whole extra regions on its own — a 1 MiB byte[] (slightly over 1 MiB once its header is
// added) hit exactly that on a 250m heap, needing roughly double the retained size in heap
// space and OOMing well before reaching the intended live-set size. 256 KiB stays comfortably
// under half of any region size G1 would pick at these heap sizes.
private static final int CHUNK_BYTES = 256 * 1024;
private static final int CHUNKS_PER_MB = (1024 * 1024) / CHUNK_BYTES;
private LoadTarget() {}
public static void main(String[] args) throws InterruptedException {
int retainMb = parseRetainMb(System.getenv(ENV_RETAIN_MB));
List<byte[]> retained = retain(retainMb);
System.out.println("load-target ready, retainedMb=" + retainMb + " chunks=" + retained.size());
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("load-target shutting down"), "load-target-shutdown"));
new CountDownLatch(1).await();
}
static int parseRetainMb(String raw) {
if (raw == null || raw.isBlank()) {
return 0;
}
return Integer.parseInt(raw.trim());
}
/**
* Allocates {@code retainMb} megabytes' worth of chunks filled with non-zero, non-compressible
* data (real pages the OS/collector can't dedupe or elide) and keeps a strong reference to
* every one, so they stay live across {@code ShrinkSequence}'s deep GC step — that's the
* point: this is simulated real application memory, not collectible garbage.
*
* <p>Package-private, not private: {@code LoadTargetTest} exercises this chunk-sizing
* arithmetic directly — the same math a real cluster run caught a humongous-object bug
* in — rather than forking a process just to prove it stays correct.
*/
static List<byte[]> retain(int retainMb) {
Random random = new Random(42); // fixed seed: deterministic fixture, not security-sensitive
int chunkCount = retainMb * CHUNKS_PER_MB;
List<byte[]> chunks = new ArrayList<>(chunkCount);
for (int i = 0; i < chunkCount; i++) {
byte[] chunk = new byte[CHUNK_BYTES];
random.nextBytes(chunk);
chunks.add(chunk);
}
return chunks;
}
}