[120] | 1 | /** |
---|
| 2 | * Licensed to the Apache Software Foundation (ASF) under one |
---|
| 3 | * or more contributor license agreements. See the NOTICE file |
---|
| 4 | * distributed with this work for additional information |
---|
| 5 | * regarding copyright ownership. The ASF licenses this file |
---|
| 6 | * to you under the Apache License, Version 2.0 (the |
---|
| 7 | * "License"); you may not use this file except in compliance |
---|
| 8 | * with the License. You may obtain a copy of the License at |
---|
| 9 | * |
---|
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 |
---|
| 11 | * |
---|
| 12 | * Unless required by applicable law or agreed to in writing, software |
---|
| 13 | * distributed under the License is distributed on an "AS IS" BASIS, |
---|
| 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
---|
| 15 | * See the License for the specific language governing permissions and |
---|
| 16 | * limitations under the License. |
---|
| 17 | */ |
---|
| 18 | |
---|
| 19 | package org.apache.hadoop.util; |
---|
| 20 | |
---|
| 21 | import java.io.BufferedWriter; |
---|
| 22 | import java.io.File; |
---|
| 23 | import java.io.FileWriter; |
---|
| 24 | import java.io.IOException; |
---|
| 25 | import java.util.Random; |
---|
| 26 | |
---|
| 27 | import org.apache.commons.logging.Log; |
---|
| 28 | import org.apache.commons.logging.LogFactory; |
---|
| 29 | import org.apache.hadoop.fs.FileUtil; |
---|
| 30 | import org.apache.hadoop.fs.Path; |
---|
| 31 | import org.apache.hadoop.util.Shell.ExitCodeException; |
---|
| 32 | import org.apache.hadoop.util.Shell.ShellCommandExecutor; |
---|
| 33 | |
---|
| 34 | import junit.framework.TestCase; |
---|
| 35 | |
---|
| 36 | public class TestProcfsBasedProcessTree extends TestCase { |
---|
| 37 | |
---|
| 38 | private static final Log LOG = LogFactory |
---|
| 39 | .getLog(TestProcfsBasedProcessTree.class); |
---|
| 40 | private static String TEST_ROOT_DIR = new Path(System.getProperty( |
---|
| 41 | "test.build.data", "/tmp")).toString().replace(' ', '+'); |
---|
| 42 | |
---|
| 43 | private ShellCommandExecutor shexec = null; |
---|
| 44 | private String pidFile; |
---|
| 45 | private String shellScript; |
---|
| 46 | private static final int N = 10; // Controls the RogueTask |
---|
| 47 | |
---|
| 48 | private static final int memoryLimit = 15 * 1024 * 1024; // 15MB |
---|
| 49 | private static final long PROCESSTREE_RECONSTRUCTION_INTERVAL = |
---|
| 50 | ProcfsBasedProcessTree.DEFAULT_SLEEPTIME_BEFORE_SIGKILL; // msec |
---|
| 51 | |
---|
| 52 | private class RogueTaskThread extends Thread { |
---|
| 53 | public void run() { |
---|
| 54 | try { |
---|
| 55 | String args[] = { "bash", "-c", |
---|
| 56 | "echo $$ > " + pidFile + "; sh " + shellScript + " " + N + ";" }; |
---|
| 57 | shexec = new ShellCommandExecutor(args); |
---|
| 58 | shexec.execute(); |
---|
| 59 | } catch (ExitCodeException ee) { |
---|
| 60 | LOG.info("Shell Command exit with a non-zero exit code. " + ee); |
---|
| 61 | } catch (IOException ioe) { |
---|
| 62 | LOG.info("Error executing shell command " + ioe); |
---|
| 63 | } finally { |
---|
| 64 | LOG.info("Exit code: " + shexec.getExitCode()); |
---|
| 65 | } |
---|
| 66 | } |
---|
| 67 | } |
---|
| 68 | |
---|
| 69 | private String getRogueTaskPID() { |
---|
| 70 | File f = new File(pidFile); |
---|
| 71 | while (!f.exists()) { |
---|
| 72 | try { |
---|
| 73 | Thread.sleep(500); |
---|
| 74 | } catch (InterruptedException ie) { |
---|
| 75 | break; |
---|
| 76 | } |
---|
| 77 | } |
---|
| 78 | |
---|
| 79 | // read from pidFile |
---|
| 80 | return ProcfsBasedProcessTree.getPidFromPidFile(pidFile); |
---|
| 81 | } |
---|
| 82 | |
---|
| 83 | public void testProcessTree() { |
---|
| 84 | |
---|
| 85 | try { |
---|
| 86 | if (!ProcfsBasedProcessTree.isAvailable()) { |
---|
| 87 | System.out |
---|
| 88 | .println("ProcfsBasedProcessTree is not available on this system. Not testing"); |
---|
| 89 | return; |
---|
| 90 | } |
---|
| 91 | } catch (Exception e) { |
---|
| 92 | LOG.info(StringUtils.stringifyException(e)); |
---|
| 93 | return; |
---|
| 94 | } |
---|
| 95 | // create shell script |
---|
| 96 | Random rm = new Random(); |
---|
| 97 | File tempFile = new File(this.getName() + "_shellScript_" + rm.nextInt() |
---|
| 98 | + ".sh"); |
---|
| 99 | tempFile.deleteOnExit(); |
---|
| 100 | shellScript = tempFile.getName(); |
---|
| 101 | |
---|
| 102 | // create pid file |
---|
| 103 | tempFile = new File(this.getName() + "_pidFile_" + rm.nextInt() + ".pid"); |
---|
| 104 | tempFile.deleteOnExit(); |
---|
| 105 | pidFile = tempFile.getName(); |
---|
| 106 | |
---|
| 107 | // write to shell-script |
---|
| 108 | try { |
---|
| 109 | FileWriter fWriter = new FileWriter(shellScript); |
---|
| 110 | fWriter.write( |
---|
| 111 | "# rogue task\n" + |
---|
| 112 | "sleep 10\n" + |
---|
| 113 | "echo hello\n" + |
---|
| 114 | "if [ $1 -ne 0 ]\n" + |
---|
| 115 | "then\n" + |
---|
| 116 | " sh " + shellScript + " $(($1-1))\n" + |
---|
| 117 | "fi"); |
---|
| 118 | fWriter.close(); |
---|
| 119 | } catch (IOException ioe) { |
---|
| 120 | LOG.info("Error: " + ioe); |
---|
| 121 | return; |
---|
| 122 | } |
---|
| 123 | |
---|
| 124 | Thread t = new RogueTaskThread(); |
---|
| 125 | t.start(); |
---|
| 126 | String pid = getRogueTaskPID(); |
---|
| 127 | LOG.info("Root process pid: " + pid); |
---|
| 128 | ProcfsBasedProcessTree p = new ProcfsBasedProcessTree(pid); |
---|
| 129 | p = p.getProcessTree(); // initialize |
---|
| 130 | try { |
---|
| 131 | while (true) { |
---|
| 132 | LOG.info("ProcessTree: " + p.toString()); |
---|
| 133 | long mem = p.getCumulativeVmem(); |
---|
| 134 | LOG.info("Memory usage: " + mem + "bytes."); |
---|
| 135 | if (mem > memoryLimit) { |
---|
| 136 | p.destroy(); |
---|
| 137 | break; |
---|
| 138 | } |
---|
| 139 | Thread.sleep(PROCESSTREE_RECONSTRUCTION_INTERVAL); |
---|
| 140 | p = p.getProcessTree(); // reconstruct |
---|
| 141 | } |
---|
| 142 | } catch (InterruptedException ie) { |
---|
| 143 | LOG.info("Interrupted."); |
---|
| 144 | } |
---|
| 145 | |
---|
| 146 | assertFalse("ProcessTree must have been gone", p.isAlive()); |
---|
| 147 | |
---|
| 148 | // Not able to join thread sometimes when forking with large N. |
---|
| 149 | try { |
---|
| 150 | t.join(2000); |
---|
| 151 | LOG.info("RogueTaskThread successfully joined."); |
---|
| 152 | } catch (InterruptedException ie) { |
---|
| 153 | LOG.info("Interrupted while joining RogueTaskThread."); |
---|
| 154 | } |
---|
| 155 | |
---|
| 156 | // ProcessTree is gone now. Any further calls should be sane. |
---|
| 157 | p = p.getProcessTree(); |
---|
| 158 | assertFalse("ProcessTree must have been gone", p.isAlive()); |
---|
| 159 | assertTrue("Cumulative vmem for the gone-process is " |
---|
| 160 | + p.getCumulativeVmem() + " . It should be zero.", p |
---|
| 161 | .getCumulativeVmem() == 0); |
---|
| 162 | assertTrue(p.toString().equals("[ ]")); |
---|
| 163 | } |
---|
| 164 | |
---|
| 165 | public static class ProcessStatInfo { |
---|
| 166 | // sample stat in a single line : 3910 (gpm) S 1 3910 3910 0 -1 4194624 |
---|
| 167 | // 83 0 0 0 0 0 0 0 16 0 1 0 7852 2408448 88 4294967295 134512640 |
---|
| 168 | // 134590050 3220521392 3220520036 10975138 0 0 4096 134234626 |
---|
| 169 | // 4294967295 0 0 17 1 0 0 |
---|
| 170 | String pid; |
---|
| 171 | String name; |
---|
| 172 | String ppid; |
---|
| 173 | String pgrpId; |
---|
| 174 | String session; |
---|
| 175 | String vmem; |
---|
| 176 | |
---|
| 177 | public ProcessStatInfo(String[] statEntries) { |
---|
| 178 | pid = statEntries[0]; |
---|
| 179 | name = statEntries[1]; |
---|
| 180 | ppid = statEntries[2]; |
---|
| 181 | pgrpId = statEntries[3]; |
---|
| 182 | session = statEntries[4]; |
---|
| 183 | vmem = statEntries[5]; |
---|
| 184 | } |
---|
| 185 | |
---|
| 186 | // construct a line that mimics the procfs stat file. |
---|
| 187 | // all unused numerical entries are set to 0. |
---|
| 188 | public String getStatLine() { |
---|
| 189 | return String.format("%s (%s) S %s %s %s 0 0 0" + |
---|
| 190 | " 0 0 0 0 0 0 0 0 0 0 0 0 0 %s 0 0 0" + |
---|
| 191 | " 0 0 0 0 0 0 0 0" + |
---|
| 192 | " 0 0 0 0 0", |
---|
| 193 | pid, name, ppid, pgrpId, session, vmem); |
---|
| 194 | } |
---|
| 195 | } |
---|
| 196 | |
---|
| 197 | /** |
---|
| 198 | * A basic test that creates a few process directories and writes |
---|
| 199 | * stat files. Verifies that the virtual memory is correctly |
---|
| 200 | * computed. |
---|
| 201 | * @throws IOException if there was a problem setting up the |
---|
| 202 | * fake procfs directories or files. |
---|
| 203 | */ |
---|
| 204 | public void testVirtualMemoryForProcessTree() throws IOException { |
---|
| 205 | |
---|
| 206 | // test processes |
---|
| 207 | String[] pids = { "100", "200", "300", "400" }; |
---|
| 208 | // create the fake procfs root directory. |
---|
| 209 | File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
---|
| 210 | |
---|
| 211 | try { |
---|
| 212 | setupProcfsRootDir(procfsRootDir); |
---|
| 213 | setupPidDirs(procfsRootDir, pids); |
---|
| 214 | |
---|
| 215 | // create stat objects. |
---|
| 216 | // assuming processes 100, 200, 300 are in tree and 400 is not. |
---|
| 217 | ProcessStatInfo[] procInfos = new ProcessStatInfo[4]; |
---|
| 218 | procInfos[0] = new ProcessStatInfo(new String[] |
---|
| 219 | {"100", "proc1", "1", "100", "100", "100000"}); |
---|
| 220 | procInfos[1] = new ProcessStatInfo(new String[] |
---|
| 221 | {"200", "proc2", "100", "100", "100", "200000"}); |
---|
| 222 | procInfos[2] = new ProcessStatInfo(new String[] |
---|
| 223 | {"300", "proc3", "200", "100", "100", "300000"}); |
---|
| 224 | procInfos[3] = new ProcessStatInfo(new String[] |
---|
| 225 | {"400", "proc4", "1", "400", "400", "400000"}); |
---|
| 226 | |
---|
| 227 | writeStatFiles(procfsRootDir, pids, procInfos); |
---|
| 228 | |
---|
| 229 | // crank up the process tree class. |
---|
| 230 | ProcfsBasedProcessTree processTree = |
---|
| 231 | new ProcfsBasedProcessTree("100", procfsRootDir.getAbsolutePath()); |
---|
| 232 | // build the process tree. |
---|
| 233 | processTree.getProcessTree(); |
---|
| 234 | |
---|
| 235 | // verify cumulative memory |
---|
| 236 | assertEquals("Cumulative memory does not match", |
---|
| 237 | Long.parseLong("600000"), processTree.getCumulativeVmem()); |
---|
| 238 | } finally { |
---|
| 239 | FileUtil.fullyDelete(procfsRootDir); |
---|
| 240 | } |
---|
| 241 | } |
---|
| 242 | |
---|
| 243 | /** |
---|
| 244 | * Tests that cumulative memory is computed only for |
---|
| 245 | * processes older than a given age. |
---|
| 246 | * @throws IOException if there was a problem setting up the |
---|
| 247 | * fake procfs directories or files. |
---|
| 248 | */ |
---|
| 249 | public void testVMemForOlderProcesses() throws IOException { |
---|
| 250 | // initial list of processes |
---|
| 251 | String[] pids = { "100", "200", "300", "400" }; |
---|
| 252 | // create the fake procfs root directory. |
---|
| 253 | File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
---|
| 254 | |
---|
| 255 | try { |
---|
| 256 | setupProcfsRootDir(procfsRootDir); |
---|
| 257 | setupPidDirs(procfsRootDir, pids); |
---|
| 258 | |
---|
| 259 | // create stat objects. |
---|
| 260 | // assuming 100, 200 and 400 are in tree, 300 is not. |
---|
| 261 | ProcessStatInfo[] procInfos = new ProcessStatInfo[4]; |
---|
| 262 | procInfos[0] = new ProcessStatInfo(new String[] |
---|
| 263 | {"100", "proc1", "1", "100", "100", "100000"}); |
---|
| 264 | procInfos[1] = new ProcessStatInfo(new String[] |
---|
| 265 | {"200", "proc2", "100", "100", "100", "200000"}); |
---|
| 266 | procInfos[2] = new ProcessStatInfo(new String[] |
---|
| 267 | {"300", "proc3", "1", "300", "300", "300000"}); |
---|
| 268 | procInfos[3] = new ProcessStatInfo(new String[] |
---|
| 269 | {"400", "proc4", "100", "100", "100", "400000"}); |
---|
| 270 | |
---|
| 271 | writeStatFiles(procfsRootDir, pids, procInfos); |
---|
| 272 | |
---|
| 273 | // crank up the process tree class. |
---|
| 274 | ProcfsBasedProcessTree processTree = |
---|
| 275 | new ProcfsBasedProcessTree("100", procfsRootDir.getAbsolutePath()); |
---|
| 276 | // build the process tree. |
---|
| 277 | processTree.getProcessTree(); |
---|
| 278 | |
---|
| 279 | // verify cumulative memory |
---|
| 280 | assertEquals("Cumulative memory does not match", |
---|
| 281 | Long.parseLong("700000"), processTree.getCumulativeVmem()); |
---|
| 282 | |
---|
| 283 | // write one more process as child of 100. |
---|
| 284 | String[] newPids = { "500" }; |
---|
| 285 | setupPidDirs(procfsRootDir, newPids); |
---|
| 286 | |
---|
| 287 | ProcessStatInfo[] newProcInfos = new ProcessStatInfo[1]; |
---|
| 288 | newProcInfos[0] = new ProcessStatInfo(new String[] |
---|
| 289 | {"500", "proc5", "100", "100", "100", "500000"}); |
---|
| 290 | writeStatFiles(procfsRootDir, newPids, newProcInfos); |
---|
| 291 | |
---|
| 292 | // check vmem includes the new process. |
---|
| 293 | processTree.getProcessTree(); |
---|
| 294 | assertEquals("Cumulative memory does not include new process", |
---|
| 295 | Long.parseLong("1200000"), processTree.getCumulativeVmem()); |
---|
| 296 | |
---|
| 297 | // however processes older than 1 iteration will retain the older value |
---|
| 298 | assertEquals("Cumulative memory shouldn't have included new process", |
---|
| 299 | Long.parseLong("700000"), processTree.getCumulativeVmem(1)); |
---|
| 300 | |
---|
| 301 | // one more process |
---|
| 302 | newPids = new String[]{ "600" }; |
---|
| 303 | setupPidDirs(procfsRootDir, newPids); |
---|
| 304 | |
---|
| 305 | newProcInfos = new ProcessStatInfo[1]; |
---|
| 306 | newProcInfos[0] = new ProcessStatInfo(new String[] |
---|
| 307 | {"600", "proc6", "100", "100", "100", "600000"}); |
---|
| 308 | writeStatFiles(procfsRootDir, newPids, newProcInfos); |
---|
| 309 | |
---|
| 310 | // refresh process tree |
---|
| 311 | processTree.getProcessTree(); |
---|
| 312 | |
---|
| 313 | // processes older than 2 iterations should be same as before. |
---|
| 314 | assertEquals("Cumulative memory shouldn't have included new processes", |
---|
| 315 | Long.parseLong("700000"), processTree.getCumulativeVmem(2)); |
---|
| 316 | |
---|
| 317 | // processes older than 1 iteration should not include new process, |
---|
| 318 | // but include process 500 |
---|
| 319 | assertEquals("Cumulative memory shouldn't have included new processes", |
---|
| 320 | Long.parseLong("1200000"), processTree.getCumulativeVmem(1)); |
---|
| 321 | |
---|
| 322 | // no processes older than 3 iterations, this should be 0 |
---|
| 323 | assertEquals("Getting non-zero vmem for processes older than 3 iterations", |
---|
| 324 | 0L, processTree.getCumulativeVmem(3)); |
---|
| 325 | } finally { |
---|
| 326 | FileUtil.fullyDelete(procfsRootDir); |
---|
| 327 | } |
---|
| 328 | } |
---|
| 329 | |
---|
| 330 | /** |
---|
| 331 | * Create a directory to mimic the procfs file system's root. |
---|
| 332 | * @param procfsRootDir root directory to create. |
---|
| 333 | * @throws IOException if could not delete the procfs root directory |
---|
| 334 | */ |
---|
| 335 | public static void setupProcfsRootDir(File procfsRootDir) |
---|
| 336 | throws IOException { |
---|
| 337 | // cleanup any existing process root dir. |
---|
| 338 | if (procfsRootDir.exists()) { |
---|
| 339 | assertTrue(FileUtil.fullyDelete(procfsRootDir)); |
---|
| 340 | } |
---|
| 341 | |
---|
| 342 | // create afresh |
---|
| 343 | assertTrue(procfsRootDir.mkdirs()); |
---|
| 344 | } |
---|
| 345 | |
---|
| 346 | /** |
---|
| 347 | * Create PID directories under the specified procfs root directory |
---|
| 348 | * @param procfsRootDir root directory of procfs file system |
---|
| 349 | * @param pids the PID directories to create. |
---|
| 350 | * @throws IOException If PID dirs could not be created |
---|
| 351 | */ |
---|
| 352 | public static void setupPidDirs(File procfsRootDir, String[] pids) |
---|
| 353 | throws IOException { |
---|
| 354 | for (String pid : pids) { |
---|
| 355 | File pidDir = new File(procfsRootDir, pid); |
---|
| 356 | pidDir.mkdir(); |
---|
| 357 | if (!pidDir.exists()) { |
---|
| 358 | throw new IOException ("couldn't make process directory under " + |
---|
| 359 | "fake procfs"); |
---|
| 360 | } else { |
---|
| 361 | LOG.info("created pid dir"); |
---|
| 362 | } |
---|
| 363 | } |
---|
| 364 | } |
---|
| 365 | |
---|
| 366 | /** |
---|
| 367 | * Write stat files under the specified pid directories with data |
---|
| 368 | * setup in the corresponding ProcessStatInfo objects |
---|
| 369 | * @param procfsRootDir root directory of procfs file system |
---|
| 370 | * @param pids the PID directories under which to create the stat file |
---|
| 371 | * @param procs corresponding ProcessStatInfo objects whose data should be |
---|
| 372 | * written to the stat files. |
---|
| 373 | * @throws IOException if stat files could not be written |
---|
| 374 | */ |
---|
| 375 | public static void writeStatFiles(File procfsRootDir, String[] pids, |
---|
| 376 | ProcessStatInfo[] procs) throws IOException { |
---|
| 377 | for (int i=0; i<pids.length; i++) { |
---|
| 378 | File statFile = new File(new File(procfsRootDir, pids[i]), "stat"); |
---|
| 379 | BufferedWriter bw = null; |
---|
| 380 | try { |
---|
| 381 | FileWriter fw = new FileWriter(statFile); |
---|
| 382 | bw = new BufferedWriter(fw); |
---|
| 383 | bw.write(procs[i].getStatLine()); |
---|
| 384 | LOG.info("wrote stat file for " + pids[i] + |
---|
| 385 | " with contents: " + procs[i].getStatLine()); |
---|
| 386 | } finally { |
---|
| 387 | // not handling exception - will throw an error and fail the test. |
---|
| 388 | if (bw != null) { |
---|
| 389 | bw.close(); |
---|
| 390 | } |
---|
| 391 | } |
---|
| 392 | } |
---|
| 393 | } |
---|
| 394 | } |
---|