Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
80814ff
YARN-11823: add new endpoints for getting jstacks of application and …
Jun 5, 2025
86a99ec
Adding number of jstack through rest api
Jun 16, 2025
3709a54
Fix the integer data type and remove the plain text output to allow e…
Jun 16, 2025
c3176f0
Merge branch 'apache:trunk' into trunk_YARN-11823
Hean-Chhinling Dec 9, 2025
fee2f9e
Clean get script location and import each module individually
Hean-Chhinling Dec 10, 2025
38c757f
Add Unit-test for the new diagnostic service class
Hean-Chhinling Dec 10, 2025
0157b0f
YARN-11823: fix the script for collection jstack application
Hean-Chhinling Feb 2, 2026
0a3300c
YARN-11823: use shellExecutor remove python script and guard the endp…
Hean-Chhinling Feb 5, 2026
824dfa1
YARN-11823: update handling exceptions, app_id checks and change the …
Hean-Chhinling Feb 12, 2026
ff7dde6
YARN-11823: use processHandle to get NM process ID and fixing the Jst…
Hean-Chhinling Feb 17, 2026
7b3ca17
YARN-11823: update to get process from NMcontext
Hean-Chhinling Feb 20, 2026
a26987c
YARN-11823: Get the container process id from NodeManager Context
Hean-Chhinling Feb 20, 2026
0a60eb2
YARN-11823: remove unused method and improve code
Hean-Chhinling Feb 24, 2026
62f76d6
YARN-11823: Add unit test for the DiagnosticJStackService
Hean-Chhinling Feb 26, 2026
3f28796
YARN-11823: fix checkstyle and bug issue
Hean-Chhinling Feb 27, 2026
cf2253b
YARN-11823: clean up code at getApplicationPID to be more readable
Hean-Chhinling Mar 2, 2026
a57c522
YARN-11823: add a working solution without using sudo
Hean-Chhinling Mar 4, 2026
25ac6e1
YARN-11823: fix the absolute path jstack issue
Hean-Chhinling Mar 5, 2026
24d413e
Merge branch 'trunk' into trunk_YARN-11823
Hean-Chhinling Mar 6, 2026
0dd4870
YARN-11823: Clean up code to use ContainerExecutor effectively
Hean-Chhinling Mar 6, 2026
75bcbab
YARN-11823: cleaned up code and to display containerId along with pro…
Hean-Chhinling Mar 9, 2026
a455836
YARN-11823: remove catching general exception exceptions and add text…
Hean-Chhinling Mar 10, 2026
3431c95
YARN-11823: check application authorization and put the non java cont…
Hean-Chhinling Mar 13, 2026
dd0c140
YARN-11823: fix handling the exception
Hean-Chhinling Mar 17, 2026
fdff845
YARN-11823: fix the exception response
Hean-Chhinling Mar 17, 2026
5168e01
YARN-11823: Use commandLine to get the java process and handle except…
Hean-Chhinling Mar 20, 2026
563278a
YARN-11823: fix unit-test after adding security measure
Hean-Chhinling Mar 23, 2026
9f76e75
YARN-11823: Handle the PID no longer exist without exception to not b…
Hean-Chhinling Mar 23, 2026
cde856a
YARN-11823: Add a check for AdminACL to run NodeManager Jstack
Hean-Chhinling Mar 23, 2026
86abf94
YARN-11823: Update to match exactly java process and handle exception…
Hean-Chhinling Mar 27, 2026
48f5ad4
YARN-11823: fix checkstyle and bug warning
Hean-Chhinling Mar 29, 2026
391abdd
YARN-11823: Fix checkstyle
Hean-Chhinling Apr 3, 2026
76d9973
YARN-11823: Add documentation for the two new endpoints and remove so…
Hean-Chhinling Apr 23, 2026
563d26e
YARN-11823: Fixed check style problem
Hean-Chhinling Apr 23, 2026
a400f5d
YARN-11823: Fixed checkstyle
Hean-Chhinling Apr 27, 2026
3afde29
YARN-11823: Fix checkstyle indentation
Hean-Chhinling Apr 28, 2026
4af1089
YARN-11823: Add a unit-test and display the command
Hean-Chhinling Sep 3, 2026
3d40555
Merge remote-tracking branch 'ling_upstream/trunk_YARN-11823' into tr…
Hean-Chhinling Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,11 @@ public static boolean isAclEnabled(Configuration conf) {
public static final String LINUX_CONTAINER_RUNTIME_PREFIX = NM_PREFIX +
"runtime.linux.";

/** Flag to turn on/off jstack endpoints for NodeManager. By default is False **/
public static final String NM_JSTACK_ENDPOINTS_ENABLED =
Comment thread
Hean-Chhinling marked this conversation as resolved.
NM_PREFIX + "jstack-endpoints.enabled";
Comment thread
Hean-Chhinling marked this conversation as resolved.
public static final boolean DEFAULT_NM_JSTACK_ENDPOINTS_ENABLED = false;

/**
* Comma separated list of runtimes that are allowed when using
* LinuxContainerExecutor. The standard values are:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5856,6 +5856,17 @@
<value>50000</value>
</property>


<property>
<description>
This configuration will be used to turn on/turn off nodemanager jstack endpoints
/ws/v1/node/jstack/{numberOfJStack} and /ws/v1/node/apps/{appid}/jstack/{numberOfJStack}.
The purpose is to minimise security risk. By default is set to false.
</description>
<name>yarn.nodemanager.jstack-endpoints.enabled</name>
<value>false</value>
</property>

<property>
<description>
Set the connect timeout interval, in milliseconds.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/** * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hadoop.yarn.server.nodemanager.webapp;


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.yarn.api.records.ApplicationAccessType;
import org.apache.hadoop.yarn.api.records.ApplicationId;
import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.exceptions.YarnRuntimeException;
import org.apache.hadoop.yarn.server.nodemanager.Context;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.application.Application;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.privileged.PrivilegedOperationExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.Optional;
import java.util.Arrays;

public class DiagnosticJStackService {

private static final Logger LOG = LoggerFactory.getLogger(DiagnosticJStackService.class);
Comment thread
Hean-Chhinling marked this conversation as resolved.

Comment thread
Hean-Chhinling marked this conversation as resolved.
private static final String NM_USER = System.getProperty("user.name");
private static final String JSTACK_PATH = System.getProperty("java.home") + "/bin/jstack";
private final Context context;
private final Configuration conf;

public DiagnosticJStackService(Context context) {
this.context = context;
this.conf = context.getConf();
}

public String collectNodeThreadDump(int numberOfJStack, HttpServletRequest req)
throws IOException {
checkShellNotWindows();

long nodeManagerPid = ProcessHandle.current().pid();

checkAdminACL(req);

return runJStack(nodeManagerPid, numberOfJStack);
}

private void checkAdminACL(HttpServletRequest req) throws IOException {
UserGroupInformation callerUGI = getUserGroupInformation(req);

boolean isAdmin = context.getApplicationACLsManager().isAdmin(callerUGI);

if (!isAdmin) {
throw new YarnRuntimeException("User " + callerUGI.getShortUserName() +
" is not authorized to run jstack on NodeManager ");
}
}

public String collectApplicationThreadDump(
String appId, int numberOfJStack, HttpServletRequest req)
throws IOException {
checkShellNotWindows();

ApplicationId applicationId = ApplicationId.fromString(appId);
Comment thread
Hean-Chhinling marked this conversation as resolved.

Application app = context.getApplications().get(applicationId);
if (app == null){
throw new YarnRuntimeException("Application " + applicationId + " does not exist");
}

checkApplicationACL(req, app);

Map<ContainerId, List<Long>> containerPids = getApplicationContainerPids(app);

return runJStack(containerPids, numberOfJStack);
}

private void checkApplicationACL(HttpServletRequest req, Application app) throws IOException {
UserGroupInformation callerUGI = getUserGroupInformation(req);

boolean isAuthorized = context.getApplicationACLsManager().checkAccess(
callerUGI, ApplicationAccessType.VIEW_APP, app.getUser(), app.getAppId()
);

if(!isAuthorized){
throw new YarnRuntimeException("User " + callerUGI.getShortUserName() +
" is not authorized to view application " + app.getAppId());
}

}

private void checkShellNotWindows() {
if (Shell.WINDOWS) {
throw new UnsupportedOperationException("Not implemented for Windows.");
}
}

protected Map<ContainerId, List<Long>> getApplicationContainerPids(Application app){
Map<ContainerId, List<Long>> containerPids = new HashMap<>();

for (ContainerId containerId : app.getContainers().keySet()){
String pidForContainerIdStr = context.getContainerExecutor().getProcessId(containerId);
long parentPid = Long.parseLong(pidForContainerIdStr);

List<Long> javaContainerPids = ProcessHandle.of(parentPid).stream()
.flatMap(ProcessHandle::descendants)
.filter(childProcess -> {
String cmdLine = childProcess.info().commandLine().orElse("").trim();
if (cmdLine.isEmpty()){
return false;
}
String executable = cmdLine.split("\\s+")[0];
// The first token is always the executable binary
return executable.equals("java") || executable.endsWith("/java");
})
.map(ProcessHandle::pid)
.toList();

containerPids.put(containerId, javaContainerPids);

}

LOG.info("Application PIDs by ContainerId: {}", containerPids);

return containerPids;
}

private String runJStack(Map<ContainerId, List<Long>> containerPids, int numJStacks){
StringBuilder result = new StringBuilder();

for(Map.Entry<ContainerId, List<Long>> entry : containerPids.entrySet()){
ContainerId containerId = entry.getKey();
List<Long> javaContainerPids = entry.getValue();

if (javaContainerPids.isEmpty()){
result.append(String.format("=== Thread Dumps for ContainerId: %s%n is skipped " +
"because no Java Process ID exist ===", containerId.toString()));
} else {
for (Long pid : javaContainerPids) {
result.append(String.format(
"=== Thread Dumps for ContainerId: %s, PID: %d ===%n%s%n",
containerId.toString(), pid, runJStack(pid, numJStacks)));
}
}

}

return result.toString();
}

private String runJStack(long pid, int numJStacks) {
Optional<ProcessHandle> processHandleOpt = ProcessHandle.of(pid);

if (processHandleOpt.isEmpty()){
String msg = String.format("Process with PID " + pid + " is no longer exists");
LOG.warn(msg);
return "Status: Skipped Process with PID " + msg;
}

ProcessHandle processHandle = processHandleOpt.get();

String runningUser = processHandle.info().user().orElse(NM_USER);
String containerExecutorPath =
PrivilegedOperationExecutor.getContainerExecutorExecutablePath(conf);

String[] jstackCommand = {
containerExecutorPath, "--run-jstack", runningUser, String.valueOf(pid), JSTACK_PATH
};

LOG.info("Running JStack command: {}", Arrays.toString(jstackCommand));

StringBuilder result = new StringBuilder();

for (int i = 0; i < numJStacks; i++) {
Shell.ShellCommandExecutor cmd =
new Shell.ShellCommandExecutor(jstackCommand, null, null, 60_000);

try {
cmd.execute();
result.append(String.format(
"--- JStack iteration %d for PID: %d ---%n%s%n", i, pid, cmd.getOutput()));
} catch (IOException e) {
result.append(String.format(
"Failed to run jstack on PID: " + pid + " at iteration: " + i +
" (Process likely exited before/during running jstack): " + e.getMessage()));
break;
}
}

return result.toString();
}

private UserGroupInformation getUserGroupInformation(HttpServletRequest req) throws IOException {
String remoteUser = req.getRemoteUser();
UserGroupInformation callerUGI;

if (remoteUser != null) {
callerUGI = UserGroupInformation.createRemoteUser(remoteUser);
} else {
callerUGI = UserGroupInformation.getCurrentUser(); // Fallback to current OS user
}

LOG.info("Checking ACL for Caller UGI: {}", callerUGI.toString());

return callerUGI;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.Set;

import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.yarn.exceptions.YarnRuntimeException;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.records.AuxServiceRecord;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.records.AuxServiceRecords;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.resourceplugin.ResourcePlugin;
Expand Down Expand Up @@ -107,7 +108,9 @@ public class NMWebServices {
private static RecordFactory recordFactory = RecordFactoryProvider.getRecordFactory(null);
private String redirectWSUrl;
private LogAggregationFileControllerFactory factory;
private DiagnosticJStackService diagnosticJStackService;
private boolean filterAppsByUser = false;
private boolean isJStackEndpointsEnable = false;

@javax.ws.rs.core.Context
private HttpServletRequest request;
Expand All @@ -132,6 +135,11 @@ public NMWebServices(final @javax.inject.Named("nm") Context nm,
this.filterAppsByUser = this.nmContext.getConf().getBoolean(
YarnConfiguration.FILTER_ENTITY_LIST_BY_USER,
YarnConfiguration.DEFAULT_DISPLAY_APPS_FOR_LOGGED_IN_USER);
this.isJStackEndpointsEnable = this.nmContext.getConf().getBoolean(
YarnConfiguration.NM_JSTACK_ENDPOINTS_ENABLED,
YarnConfiguration.DEFAULT_NM_JSTACK_ENDPOINTS_ENABLED
);
this.diagnosticJStackService = new DiagnosticJStackService(this.nmContext);
}

public NMWebServices(final Context nm, final ResourceView view,
Expand Down Expand Up @@ -628,6 +636,68 @@ public Response syncYarnSysFS(@javax.ws.rs.core.Context
return Response.ok().build();
}


@GET
@Path("/jstack/{numberOfJStack}")
Comment thread
Hean-Chhinling marked this conversation as resolved.
@Produces({ MediaType.TEXT_PLAIN})
public Response getNodeThreadDump(@javax.ws.rs.core.Context HttpServletRequest req,
@PathParam("numberOfJStack") int numberOfJStack) {
if (!isJStackEndpointsEnable) {
return Response.status(Status.METHOD_NOT_ALLOWED)
Comment thread
Hean-Chhinling marked this conversation as resolved.
.build();
}

try {
return Response.status(Status.OK)
.entity(diagnosticJStackService.collectNodeThreadDump(numberOfJStack, req))
.build();
} catch (YarnRuntimeException e) {
return Response.status(Status.FORBIDDEN)
.entity(e.getMessage())
.build();
} catch (IOException e){
return Response.status(Status.INTERNAL_SERVER_ERROR)
.entity("Shell command has failed: " + e.getMessage() + ". " +
"For more information please check the NodeManager logs.")
.build();
}

}


@GET
@Path("/apps/{appid}/jstack/{numberOfJStack}")
@Produces({MediaType.TEXT_PLAIN})
public Response getApplicationJStack(@javax.ws.rs.core.Context HttpServletRequest req,
@PathParam("appid") String appId,
@PathParam("numberOfJStack") int numberOfJStack) {
if (!isJStackEndpointsEnable) {
return Response.status(Status.METHOD_NOT_ALLOWED)
.build();
}

try {
return Response.status(Status.OK)
.entity(diagnosticJStackService
.collectApplicationThreadDump(appId, numberOfJStack, req))
.build();
} catch (IllegalArgumentException e){
return Response.status(Status.BAD_REQUEST)
.entity("The applicationId is invalid: " + appId + ". " + e.getMessage())
.build();
} catch (YarnRuntimeException e) {
return Response.status(Status.FORBIDDEN)
.entity(e.getMessage())
.build();
} catch (IOException e){
return Response.status(Status.INTERNAL_SERVER_ERROR)
.entity("Shell command has failed: " + e.getMessage() + ". " +
"For more information please check the NodeManager logs.")
.build();
}

}

private long parseLongParam(String bytes) {
if (bytes == null || bytes.isEmpty()) {
return Long.MAX_VALUE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3310,3 +3310,16 @@ int remove_docker_container(char**argv, int argc) {
}
return exit_code;
}

int run_jstack_as_user(const char *user, const char *pid, const char *jstack_path) {
int exit_code = set_user(user);
if (exit_code != 0) {
fprintf(ERRORFILE, "Failed to set user to %s\n", user);
return exit_code;
}

execl(jstack_path, "jstack", pid, (char *) NULL);

fprintf(LOGFILE, "Failed to execute jstack: %s\n", strerror(errno));
return UNABLE_TO_EXECUTE_CONTAINER_SCRIPT;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ enum operations {
RUN_AS_USER_SYNC_YARN_SYSFS = 15,
EXEC_CONTAINER = 16,
RUN_RUNC_CONTAINER = 17,
REAP_RUNC_LAYER_MOUNTS = 18
REAP_RUNC_LAYER_MOUNTS = 18,
RUN_JSTACK = 19
};

#define NM_GROUP_KEY "yarn.nodemanager.linux-container-executor.group"
Expand Down Expand Up @@ -368,3 +369,8 @@ int is_terminal_support_enabled();
* Check if runC feature is enabled
*/
int is_runc_support_enabled();

/**
* Run jstack as a specific user.
*/
int run_jstack_as_user(const char *user, const char *pid, const char *jstack_path);
Loading
Loading