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 | } |
---|