Skip to content

Commit 2a3e926

Browse files
jbachorikclaude
andcommitted
Extend MemoryGovernor to heap usage and 5 more antagonists; bump chaos heap to 3072m
Cgroup-only signal missed heap exhaustion when Xmx is a small fraction of the cgroup limit (job 1860026763). Governor now also watches heap usage; pace() wired into weakref-wave, hidden-class-churn, context-hop, bounded-pool, consumer-group alongside the existing alloc-storm/direct-memory/dump-storm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent ae3daa1 commit 2a3e926

7 files changed

Lines changed: 69 additions & 29 deletions

File tree

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/BoundedThreadPoolAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ private void schedulePoolTasks(final int poolIdx) {
9696
@Override
9797
public void run() {
9898
if (!running) return;
99+
MemoryGovernor.pace();
99100
long seed = System.nanoTime();
100101
sink.addAndGet(burn(seed));
101102
ScheduledExecutorService sibling = pools[nextIdx];

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/ConsumerGroupAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ private void rebalanceLoop() {
104104
return;
105105
}
106106
if (!running) return;
107+
MemoryGovernor.pace();
107108

108109
// Interrupt all victims simultaneously (burst)
109110
Thread[] victims = new Thread[BURST_SIZE];

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/ContextHopAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void stopGracefully(Duration timeout) {
7676

7777
private void startChain(final int chainId) {
7878
if (!running) return;
79+
MemoryGovernor.pace();
7980
final long seed = ThreadLocalRandom.current().nextLong() ^ ((long) chainId << 32);
8081
CompletableFuture
8182
.runAsync(new Runnable() {

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/HiddenClassChurnAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ private void loop() {
110110
Thread.currentThread().interrupt();
111111
return;
112112
}
113+
MemoryGovernor.pace();
113114
}
114115
}
115116

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/MemoryGovernor.java

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,43 @@
1010
package com.datadoghq.profiler.chaos;
1111

1212
import java.io.IOException;
13+
import java.lang.management.ManagementFactory;
14+
import java.lang.management.MemoryUsage;
1315
import java.nio.charset.StandardCharsets;
1416
import java.nio.file.Files;
1517
import java.nio.file.Path;
1618
import java.nio.file.Paths;
1719

1820
/**
19-
* Shared memory-pressure gate for antagonists that burst allocations fast
20-
* enough to threaten the container's cgroup ceiling before GC/Cleaner
21-
* catches up (alloc-storm, direct-memory, dump-storm).
21+
* Shared memory-pressure gate for antagonists whose allocation/thread-churn
22+
* rate can outrun GC/cleanup and threaten either the container's cgroup
23+
* ceiling or the JVM heap itself (alloc-storm, direct-memory, dump-storm,
24+
* weakref-wave, hidden-class-churn, context-hop, bounded-pool,
25+
* consumer-group).
2226
*
23-
* <p>A single background thread samples the cgroup memory usage/limit (v2
24-
* {@code memory.current}/{@code memory.max}, falling back to v1 {@code
25-
* memory.usage_in_bytes}/{@code memory.limit_in_bytes}) and flips a shared
26-
* {@code throttled} flag with hysteresis: entering throttle above {@link
27-
* #HIGH_WATERMARK} of the limit, clearing it below {@link #LOW_WATERMARK}.
28-
* Antagonists poll {@link #pace()} once per loop iteration — a volatile read
29-
* plus, only while throttled, a short sleep. If no cgroup limit is readable
30-
* (e.g. running outside a container), the governor stays permanently
31-
* disabled rather than guessing at a ceiling.
27+
* <p>A single background thread samples two independent signals every tick:
28+
* cgroup memory usage/limit (v2 {@code memory.current}/{@code memory.max},
29+
* falling back to v1 {@code memory.usage_in_bytes}/{@code
30+
* memory.limit_in_bytes}) and JVM heap usage ({@link
31+
* java.lang.management.MemoryMXBean#getHeapMemoryUsage()}). Either signal
32+
* crossing its high watermark sets the shared {@code throttled} flag;
33+
* clearing it requires both signals to be back below their low watermark
34+
* (hysteresis, so a single antagonist bouncing near one ceiling doesn't
35+
* flap the gate for antagonists driving the other). A signal that has no
36+
* readable ceiling (no cgroup limit, or a JVM without {@code -Xmx}) is
37+
* treated as permanently low rather than guessed at.
38+
*
39+
* <p>Antagonists poll {@link #pace()} once per loop iteration — a volatile
40+
* read plus, only while throttled, a short sleep.
3241
*/
3342
final class MemoryGovernor {
3443

35-
private static final double HIGH_WATERMARK = 0.85;
36-
private static final double LOW_WATERMARK = 0.70;
44+
private static final double CGROUP_HIGH_WATERMARK = 0.85;
45+
private static final double CGROUP_LOW_WATERMARK = 0.70;
46+
// Heap-space OOME is less recoverable than a cgroup kill (no headroom to
47+
// shed off-heap first), so back off earlier and require more slack.
48+
private static final double HEAP_HIGH_WATERMARK = 0.80;
49+
private static final double HEAP_LOW_WATERMARK = 0.65;
3750
private static final long SAMPLE_INTERVAL_MS = 500;
3851
private static final long THROTTLE_SLEEP_MS = 5;
3952

@@ -51,14 +64,17 @@ final class MemoryGovernor {
5164
private MemoryGovernor() {
5265
}
5366

54-
/** Starts the background sampler if a cgroup memory limit is readable. Idempotent-ish: call once from Main. */
67+
/** Starts the background sampler. Cgroup and/or heap signals no-op individually if unreadable. Call once from Main. */
5568
static void start() {
5669
Path usagePath = firstReadable(USAGE_PATHS);
5770
long limitBytes = firstLimit(LIMIT_PATHS);
5871
if (usagePath == null || limitBytes <= 0) {
59-
return; // no container ceiling visible; never throttle
72+
usagePath = null;
73+
limitBytes = -1;
6074
}
61-
Thread sampler = new Thread(() -> sampleLoop(usagePath, limitBytes), "chaos-memory-governor");
75+
Path finalUsagePath = usagePath;
76+
long finalLimitBytes = limitBytes;
77+
Thread sampler = new Thread(() -> sampleLoop(finalUsagePath, finalLimitBytes), "chaos-memory-governor");
6278
sampler.setDaemon(true);
6379
sampler.start();
6480
}
@@ -78,20 +94,37 @@ private static void sampleLoop(Path usagePath, long limitBytes) {
7894
while (true) {
7995
try {
8096
Thread.sleep(SAMPLE_INTERVAL_MS);
81-
long usage = readLong(usagePath);
82-
double fraction = (double) usage / (double) limitBytes;
83-
if (fraction >= HIGH_WATERMARK) {
84-
throttled = true;
85-
} else if (fraction <= LOW_WATERMARK) {
86-
throttled = false;
87-
}
8897
} catch (InterruptedException e) {
8998
Thread.currentThread().interrupt();
9099
return;
91-
} catch (IOException | NumberFormatException e) {
92-
// Transient read failure; keep last known state and retry next tick.
93100
}
101+
try {
102+
update(usagePath, limitBytes);
103+
} catch (IOException | NumberFormatException | OutOfMemoryError e) {
104+
// Transient read/allocation failure; keep last known state and retry next tick.
105+
}
106+
}
107+
}
108+
109+
private static void update(Path usagePath, long limitBytes) throws IOException {
110+
double cgroupFraction = 0.0;
111+
if (usagePath != null) {
112+
cgroupFraction = (double) readLong(usagePath) / (double) limitBytes;
113+
}
114+
double heapFraction = 0.0;
115+
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
116+
if (heap.getMax() > 0) {
117+
heapFraction = (double) heap.getUsed() / (double) heap.getMax();
118+
}
119+
120+
boolean high = cgroupFraction >= CGROUP_HIGH_WATERMARK || heapFraction >= HEAP_HIGH_WATERMARK;
121+
boolean low = cgroupFraction <= CGROUP_LOW_WATERMARK && heapFraction <= HEAP_LOW_WATERMARK;
122+
if (high) {
123+
throttled = true;
124+
} else if (low) {
125+
throttled = false;
94126
}
127+
// else: in the dead zone between watermarks — keep current state.
95128
}
96129

97130
private static Path firstReadable(Path[] candidates) {

ddprof-stresstest/src/chaos/java/com/datadoghq/profiler/chaos/WeakRefWaveAntagonist.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private void waveLoop() {
8484
obj[0] = (byte) i;
8585
strongRefs.add(obj);
8686
weakRefs.add(new WeakReference<byte[]>(obj));
87+
MemoryGovernor.pace();
8788
}
8889
// Publish filled list to reader
8990
currentWave = weakRefs;

utils/run-chaos-harness.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,11 @@ echo "cgroup memory limit: ${CGROUP_MEM_LIMIT}"
194194
# dump-storm) deliberately burst off-heap/native memory hard and fast, sharing
195195
# the same cgroup limit as the heap. Reproduced under a 6GiB-capped cgroup and
196196
# confirmed MemoryGovernor (see chaos/MemoryGovernor.java, wired into the
197-
# antagonists' hot loops) keeps that same config under the limit, so the heap
198-
# no longer needs to be trimmed to make room for off-heap bursts.
199-
HEAP_MB=2560
197+
# antagonists' hot loops) keeps that same config under the limit. 2560m still
198+
# ran within a few percent of java.lang.OutOfMemoryError under the profiler
199+
# config (job 1860026763); 3072m keeps comparable headroom to the off-heap
200+
# side while giving the heap itself real margin.
201+
HEAP_MB=3072
200202

201203
HS_ERR="${CHAOS_ERROR_FILE:-${WORK_DIR}/hs_err.log}"
202204
rm -f "${HS_ERR}"

0 commit comments

Comments
 (0)