TestTenantHeapLimit.java 10.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

/*
 * Copyright (c) 2020 Alibaba Group Holding Limited. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation. Alibaba designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

/*
 * @test
 * @summary Test heap limit on tenant
 * @library /testlibrary /testlibrary/whitebox
 * @build TestTenantHeapLimit
 * @run main ClassFileInstaller sun.hotspot.WhiteBox
 * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+MultiTenant -XX:+TenantHeapThrottling -XX:+WhiteBoxAPI -XX:+UseG1GC -Xmx1024M -Xms512M -XX:G1HeapRegionSize=1M TestTenantHeapLimit
 */

import static com.oracle.java.testlibrary.Asserts.*;
import com.alibaba.tenant.TenantConfiguration;
import com.alibaba.tenant.TenantContainer;
import com.alibaba.tenant.TenantException;
import com.oracle.java.testlibrary.OutputAnalyzer;
import com.oracle.java.testlibrary.Platform;
import com.oracle.java.testlibrary.ProcessTools;
import sun.hotspot.WhiteBox;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class TestTenantHeapLimit {

    private static final WhiteBox wb = WhiteBox.getWhiteBox();

    private static final int HEAP_REGION_SIZE = wb.g1RegionSize();

    // convenient sizing constants
    private static final int K = 1024;
    private static final int M = K * 1024;
    private static final int G = M * 1024;
    // object header size
    private static final int objHeaderSize = Platform.is64bit() ? 16 : 8;

    public static void main(String[] args) throws Exception {
        testExpectedOOM(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 9));
        testExpectedOOM(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 7));
        testExpectedOOM(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 4));
        testExpectedOOM(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 1));

        testThrowAwayGarbage(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 9), 5000L /* ms */);
        testThrowAwayGarbage(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 7), 5000L /* ms */);
        testThrowAwayGarbage(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 4), 5000L /* ms */);
        testThrowAwayGarbage(new TenantConfiguration().limitHeap(HEAP_REGION_SIZE << 1), 5000L /* ms */);

        testSmallTenantHeapLimit();
        testHumongousLimit();
    }

    private static void testHumongousLimit() {
        TenantConfiguration config = new TenantConfiguration().limitHeap(HEAP_REGION_SIZE * 100);
        TenantContainer tenant = TenantContainer.create(config);

        System.gc();

        try {
            tenant.run(()->{
                try {
                    // create a humongous object whose size > tenant heap limit
                    Object obj = new byte[(int) (config.getMaxHeap() * 2)];
                    // should throw OOM
                    fail();
                } catch (OutOfMemoryError oom) {
                    // expected!
                } catch (Throwable e) {
                    System.err.println("Unexpected exception!");
                    e.printStackTrace();
                    fail();
                }
            });
        } catch (TenantException e) {
            fail();
        }
    }

    //
    // create a tenant which has heap limit far less than current young gen size,
    // after allocating object across tenant limit, YGC should be triggered.
    //
    private static void testSmallTenantHeapLimit() {
        OutputAnalyzer output = null;
        try {
            ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(
                    "-XX:+UseG1GC",
                    "-Xmx1024m",
                    "-Xms1024m",
                    "-XX:G1HeapRegionSize=1M",
                    "-XX:+PrintGCDetails",
                    "-Xbootclasspath/a:.",
                    "-XX:+UnlockDiagnosticVMOptions",
                    "-XX:+WhiteBoxAPI",
                    "-XX:+PrintGC",
                    "-XX:+MultiTenant",
                    "-XX:+TenantHeapThrottling",
                    "-XX:+UnlockDiagnosticVMOptions",
                    "-XX:+UnlockExperimentalVMOptions",
                    "-XX:G1NewSizePercent=50", /* young gen: 512MB ~= 1024MB * 50% */
                    HeapConsumer.class.getName());
            output = ProcessTools.executeProcess(pb);

            // expect normal exit, no OOM error
            assertEquals(output.getExitValue(), 0);
            // should not have full GC
            assertFalse(output.getStdout().contains("[Full GC (Allocation Failure)"));
            // must have triggerred YGC
            assertTrue(output.getStdout().contains("[GC pause (G1 Evacuation Pause) (young)"));
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        } finally {
            if (output != null) {
                System.out.println("STDOUT of child process:");
                System.out.println(output.getStdout());
                System.out.println("STDOUT END");
                System.err.println("STDERR of child process:");
                System.err.println(output.getStderr());
                System.err.println("STDERR END");
            }
        }
    }

    public static class HeapConsumer {
        public static void main(String[] args) {
            // tenant heap limit is 40M, much smaller than young gen capacity 512M.
            // so GC should be triggered right after reaching tenant heap limit.
            TenantConfiguration config = new TenantConfiguration().limitHeap(40 * M);
            TenantContainer tenant = TenantContainer.create(config);
            try {
                tenant.run(()-> {
                    int allocationGrain = 64; /*bytes per allocation*/
                    long unitSize = allocationGrain + objHeaderSize;
                    // to keep track of total allocated object size
                    long totalAllocatedBytes = 0;
                    // total limit is far more large that tenant limit
                    long allocateLimit = 200 * M;
                    // keep liveReferencePercent of tenant limit as strongly reachable
                    float liveReferencePercent = 0.5f;
                    int slots = (int) (config.getMaxHeap() / unitSize * liveReferencePercent);
                    // strong references to prevent some of the object from being GCed
                    List<Object> holder = new ArrayList<>(slots);
                    int nextIndex = 0;

                    // allocate objects
                    while (totalAllocatedBytes < allocateLimit) {
                        // allocate object
                        byte[] arr = new byte[allocationGrain];

                        // build necessary references
                        totalAllocatedBytes += (arr.length);
                        if (holder.size() < slots) {
                            holder.add(arr);
                        } else {
                            holder.set((nextIndex++) % slots, arr);
                        }
                    }
                    System.out.println("finished allocation! total " + totalAllocatedBytes + " bytes");
                });
            } catch (TenantException e) {
                e.printStackTrace();
                fail();
            } finally {
                tenant.destroy();
            }
        }
    }

    private static void testThrowAwayGarbage(TenantConfiguration config, long millis) {
        System.out.println("Start testThrowAwayGarbage with heapLimit=" + config.getMaxHeap() + "bytes");
        final AtomicBoolean shouldEnd = new AtomicBoolean(false);
        // below well-behaved task never holds memory exceeds rebel heap limit
        Thread conformist = new Thread() {
            @Override
            public void run() {
                TenantContainer slave = TenantContainer.create(config);
                try {
                    slave.run(() -> {
                        while (!shouldEnd.get()) {
                            // allocate and throw away
                            Object o = new byte[4];
                            o = new byte[HEAP_REGION_SIZE >> 2];
                        }
                    });
                } catch (TenantException e) {
                    fail();
                } finally {
                    slave.destroy();
                }
            }
        };
        conformist.start();

        // wait for the conformist to end
        try {
            Thread.sleep(millis);
            shouldEnd.set(true);
            conformist.join();
        } catch (InterruptedException e) {
            fail();
        }
        System.out.println("testThrowAwayGarbage finished normally");
    }

    private static void testExpectedOOM(TenantConfiguration config) {
        // below is a bad tenant task, which will hold memory more that limit,
        // thus should throw OOM
        System.out.println("Start testExpectedOOM with heapLimit=" + config.getMaxHeap() + "bytes");
        TenantContainer rebel = TenantContainer.create(config);
        try {
            rebel.run(() -> {
                long total = 0, limit = (config.getMaxHeap() << 1);
                int unit = (HEAP_REGION_SIZE >> 3);
                List<Object> refs = new ArrayList();

                while (total < limit) {
                    refs.add(new byte[unit]);
                    total += unit;
                }
            });
            fail();
        } catch (TenantException e) {
            fail();
        } catch (OutOfMemoryError oom) {
            // expected!
            System.out.println("Expected OOM!");
        } catch (Throwable t) {
            // for other exception, just error
            fail();
        } finally {
            rebel.destroy();
        }
        System.out.println("testExpectedOOM finished normally");
    }

    private static void fail() {
        assertTrue(false, "Failed!");
    }
}