001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.yarn.applications.distributedshell; 020 021import java.io.BufferedReader; 022import java.io.DataInputStream; 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.io.StringReader; 027import java.lang.reflect.UndeclaredThrowableException; 028import java.net.URI; 029import java.net.URISyntaxException; 030import java.nio.ByteBuffer; 031import java.security.PrivilegedExceptionAction; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039import java.util.Vector; 040import java.util.concurrent.ConcurrentHashMap; 041import java.util.concurrent.ConcurrentMap; 042import java.util.concurrent.atomic.AtomicInteger; 043 044import org.apache.commons.cli.CommandLine; 045import org.apache.commons.cli.GnuParser; 046import org.apache.commons.cli.HelpFormatter; 047import org.apache.commons.cli.Options; 048import org.apache.commons.cli.ParseException; 049import org.apache.commons.logging.Log; 050import org.apache.commons.logging.LogFactory; 051import org.apache.hadoop.classification.InterfaceAudience; 052import org.apache.hadoop.classification.InterfaceAudience.Private; 053import org.apache.hadoop.classification.InterfaceStability; 054import org.apache.hadoop.conf.Configuration; 055import org.apache.hadoop.fs.FileSystem; 056import org.apache.hadoop.fs.Path; 057import org.apache.hadoop.io.DataOutputBuffer; 058import org.apache.hadoop.io.IOUtils; 059import org.apache.hadoop.net.NetUtils; 060import org.apache.hadoop.security.Credentials; 061import org.apache.hadoop.security.UserGroupInformation; 062import org.apache.hadoop.security.token.Token; 063import org.apache.hadoop.util.ExitUtil; 064import org.apache.hadoop.util.Shell; 065import org.apache.hadoop.yarn.api.ApplicationConstants; 066import org.apache.hadoop.yarn.api.ApplicationConstants.Environment; 067import org.apache.hadoop.yarn.api.ApplicationMasterProtocol; 068import org.apache.hadoop.yarn.api.ContainerManagementProtocol; 069import org.apache.hadoop.yarn.api.protocolrecords.AllocateRequest; 070import org.apache.hadoop.yarn.api.protocolrecords.AllocateResponse; 071import org.apache.hadoop.yarn.api.protocolrecords.FinishApplicationMasterRequest; 072import org.apache.hadoop.yarn.api.protocolrecords.RegisterApplicationMasterResponse; 073import org.apache.hadoop.yarn.api.protocolrecords.StartContainerRequest; 074import org.apache.hadoop.yarn.api.records.ApplicationAttemptId; 075import org.apache.hadoop.yarn.api.records.Container; 076import org.apache.hadoop.yarn.api.records.ContainerExitStatus; 077import org.apache.hadoop.yarn.api.records.ContainerId; 078import org.apache.hadoop.yarn.api.records.ContainerLaunchContext; 079import org.apache.hadoop.yarn.api.records.ContainerState; 080import org.apache.hadoop.yarn.api.records.ContainerStatus; 081import org.apache.hadoop.yarn.api.records.FinalApplicationStatus; 082import org.apache.hadoop.yarn.api.records.LocalResource; 083import org.apache.hadoop.yarn.api.records.LocalResourceType; 084import org.apache.hadoop.yarn.api.records.LocalResourceVisibility; 085import org.apache.hadoop.yarn.api.records.NodeReport; 086import org.apache.hadoop.yarn.api.records.Priority; 087import org.apache.hadoop.yarn.api.records.Resource; 088import org.apache.hadoop.yarn.api.records.ResourceRequest; 089import org.apache.hadoop.yarn.api.records.URL; 090import org.apache.hadoop.yarn.api.records.timeline.TimelineEntity; 091import org.apache.hadoop.yarn.api.records.timeline.TimelineEvent; 092import org.apache.hadoop.yarn.api.records.timeline.TimelinePutResponse; 093import org.apache.hadoop.yarn.client.api.AMRMClient.ContainerRequest; 094import org.apache.hadoop.yarn.client.api.TimelineClient; 095import org.apache.hadoop.yarn.client.api.async.AMRMClientAsync; 096import org.apache.hadoop.yarn.client.api.async.NMClientAsync; 097import org.apache.hadoop.yarn.client.api.async.impl.NMClientAsyncImpl; 098import org.apache.hadoop.yarn.conf.YarnConfiguration; 099import org.apache.hadoop.yarn.exceptions.YarnException; 100import org.apache.hadoop.yarn.security.AMRMTokenIdentifier; 101import org.apache.hadoop.yarn.util.ConverterUtils; 102import org.apache.log4j.LogManager; 103 104import com.google.common.annotations.VisibleForTesting; 105 106/** 107 * An ApplicationMaster for executing shell commands on a set of launched 108 * containers using the YARN framework. 109 * 110 * <p> 111 * This class is meant to act as an example on how to write yarn-based 112 * application masters. 113 * </p> 114 * 115 * <p> 116 * The ApplicationMaster is started on a container by the 117 * <code>ResourceManager</code>'s launcher. The first thing that the 118 * <code>ApplicationMaster</code> needs to do is to connect and register itself 119 * with the <code>ResourceManager</code>. The registration sets up information 120 * within the <code>ResourceManager</code> regarding what host:port the 121 * ApplicationMaster is listening on to provide any form of functionality to a 122 * client as well as a tracking url that a client can use to keep track of 123 * status/job history if needed. However, in the distributedshell, trackingurl 124 * and appMasterHost:appMasterRpcPort are not supported. 125 * </p> 126 * 127 * <p> 128 * The <code>ApplicationMaster</code> needs to send a heartbeat to the 129 * <code>ResourceManager</code> at regular intervals to inform the 130 * <code>ResourceManager</code> that it is up and alive. The 131 * {@link ApplicationMasterProtocol#allocate} to the <code>ResourceManager</code> from the 132 * <code>ApplicationMaster</code> acts as a heartbeat. 133 * 134 * <p> 135 * For the actual handling of the job, the <code>ApplicationMaster</code> has to 136 * request the <code>ResourceManager</code> via {@link AllocateRequest} for the 137 * required no. of containers using {@link ResourceRequest} with the necessary 138 * resource specifications such as node location, computational 139 * (memory/disk/cpu) resource requirements. The <code>ResourceManager</code> 140 * responds with an {@link AllocateResponse} that informs the 141 * <code>ApplicationMaster</code> of the set of newly allocated containers, 142 * completed containers as well as current state of available resources. 143 * </p> 144 * 145 * <p> 146 * For each allocated container, the <code>ApplicationMaster</code> can then set 147 * up the necessary launch context via {@link ContainerLaunchContext} to specify 148 * the allocated container id, local resources required by the executable, the 149 * environment to be setup for the executable, commands to execute, etc. and 150 * submit a {@link StartContainerRequest} to the {@link ContainerManagementProtocol} to 151 * launch and execute the defined commands on the given allocated container. 152 * </p> 153 * 154 * <p> 155 * The <code>ApplicationMaster</code> can monitor the launched container by 156 * either querying the <code>ResourceManager</code> using 157 * {@link ApplicationMasterProtocol#allocate} to get updates on completed containers or via 158 * the {@link ContainerManagementProtocol} by querying for the status of the allocated 159 * container's {@link ContainerId}. 160 * 161 * <p> 162 * After the job has been completed, the <code>ApplicationMaster</code> has to 163 * send a {@link FinishApplicationMasterRequest} to the 164 * <code>ResourceManager</code> to inform it that the 165 * <code>ApplicationMaster</code> has been completed. 166 */ 167@InterfaceAudience.Public 168@InterfaceStability.Unstable 169public class ApplicationMaster { 170 171 private static final Log LOG = LogFactory.getLog(ApplicationMaster.class); 172 173 @VisibleForTesting 174 @Private 175 public static enum DSEvent { 176 DS_APP_ATTEMPT_START, DS_APP_ATTEMPT_END, DS_CONTAINER_START, DS_CONTAINER_END 177 } 178 179 @VisibleForTesting 180 @Private 181 public static enum DSEntity { 182 DS_APP_ATTEMPT, DS_CONTAINER 183 } 184 185 private static final String YARN_SHELL_ID = "YARN_SHELL_ID"; 186 187 // Configuration 188 private Configuration conf; 189 190 // Handle to communicate with the Resource Manager 191 @SuppressWarnings("rawtypes") 192 private AMRMClientAsync amRMClient; 193 194 // In both secure and non-secure modes, this points to the job-submitter. 195 @VisibleForTesting 196 UserGroupInformation appSubmitterUgi; 197 198 // Handle to communicate with the Node Manager 199 private NMClientAsync nmClientAsync; 200 // Listen to process the response from the Node Manager 201 private NMCallbackHandler containerListener; 202 203 // Application Attempt Id ( combination of attemptId and fail count ) 204 @VisibleForTesting 205 protected ApplicationAttemptId appAttemptID; 206 207 // TODO 208 // For status update for clients - yet to be implemented 209 // Hostname of the container 210 private String appMasterHostname = ""; 211 // Port on which the app master listens for status updates from clients 212 private int appMasterRpcPort = -1; 213 // Tracking url to which app master publishes info for clients to monitor 214 private String appMasterTrackingUrl = ""; 215 216 // App Master configuration 217 // No. of containers to run shell command on 218 @VisibleForTesting 219 protected int numTotalContainers = 1; 220 // Memory to request for the container on which the shell command will run 221 private int containerMemory = 10; 222 // VirtualCores to request for the container on which the shell command will run 223 private int containerVirtualCores = 1; 224 // Priority of the request 225 private int requestPriority; 226 227 // Counter for completed containers ( complete denotes successful or failed ) 228 private AtomicInteger numCompletedContainers = new AtomicInteger(); 229 // Allocated container count so that we know how many containers has the RM 230 // allocated to us 231 @VisibleForTesting 232 protected AtomicInteger numAllocatedContainers = new AtomicInteger(); 233 // Count of failed containers 234 private AtomicInteger numFailedContainers = new AtomicInteger(); 235 // Count of containers already requested from the RM 236 // Needed as once requested, we should not request for containers again. 237 // Only request for more if the original requirement changes. 238 @VisibleForTesting 239 protected AtomicInteger numRequestedContainers = new AtomicInteger(); 240 241 // Shell command to be executed 242 private String shellCommand = ""; 243 // Args to be passed to the shell command 244 private String shellArgs = ""; 245 // Env variables to be setup for the shell command 246 private Map<String, String> shellEnv = new HashMap<String, String>(); 247 248 // Location of shell script ( obtained from info set in env ) 249 // Shell script path in fs 250 private String scriptPath = ""; 251 // Timestamp needed for creating a local resource 252 private long shellScriptPathTimestamp = 0; 253 // File length needed for local resource 254 private long shellScriptPathLen = 0; 255 256 // Timeline domain ID 257 private String domainId = null; 258 259 // Hardcoded path to shell script in launch container's local env 260 private static final String ExecShellStringPath = Client.SCRIPT_PATH + ".sh"; 261 private static final String ExecBatScripStringtPath = Client.SCRIPT_PATH 262 + ".bat"; 263 264 // Hardcoded path to custom log_properties 265 private static final String log4jPath = "log4j.properties"; 266 267 private static final String shellCommandPath = "shellCommands"; 268 private static final String shellArgsPath = "shellArgs"; 269 270 private volatile boolean done; 271 272 private ByteBuffer allTokens; 273 274 // Launch threads 275 private List<Thread> launchThreads = new ArrayList<Thread>(); 276 277 // Timeline Client 278 private TimelineClient timelineClient; 279 280 private final String linux_bash_command = "bash"; 281 private final String windows_command = "cmd /c"; 282 283 private int yarnShellIdCounter = 1; 284 285 @VisibleForTesting 286 protected final Set<ContainerId> launchedContainers = 287 Collections.newSetFromMap(new ConcurrentHashMap<ContainerId, Boolean>()); 288 289 /** 290 * @param args Command line args 291 */ 292 public static void main(String[] args) { 293 boolean result = false; 294 try { 295 ApplicationMaster appMaster = new ApplicationMaster(); 296 LOG.info("Initializing ApplicationMaster"); 297 boolean doRun = appMaster.init(args); 298 if (!doRun) { 299 System.exit(0); 300 } 301 appMaster.run(); 302 result = appMaster.finish(); 303 } catch (Throwable t) { 304 LOG.fatal("Error running ApplicationMaster", t); 305 LogManager.shutdown(); 306 ExitUtil.terminate(1, t); 307 } 308 if (result) { 309 LOG.info("Application Master completed successfully. exiting"); 310 System.exit(0); 311 } else { 312 LOG.info("Application Master failed. exiting"); 313 System.exit(2); 314 } 315 } 316 317 /** 318 * Dump out contents of $CWD and the environment to stdout for debugging 319 */ 320 private void dumpOutDebugInfo() { 321 322 LOG.info("Dump debug output"); 323 Map<String, String> envs = System.getenv(); 324 for (Map.Entry<String, String> env : envs.entrySet()) { 325 LOG.info("System env: key=" + env.getKey() + ", val=" + env.getValue()); 326 System.out.println("System env: key=" + env.getKey() + ", val=" 327 + env.getValue()); 328 } 329 330 BufferedReader buf = null; 331 try { 332 String lines = Shell.WINDOWS ? Shell.execCommand("cmd", "/c", "dir") : 333 Shell.execCommand("ls", "-al"); 334 buf = new BufferedReader(new StringReader(lines)); 335 String line = ""; 336 while ((line = buf.readLine()) != null) { 337 LOG.info("System CWD content: " + line); 338 System.out.println("System CWD content: " + line); 339 } 340 } catch (IOException e) { 341 e.printStackTrace(); 342 } finally { 343 IOUtils.cleanup(LOG, buf); 344 } 345 } 346 347 public ApplicationMaster() { 348 // Set up the configuration 349 conf = new YarnConfiguration(); 350 } 351 352 /** 353 * Parse command line options 354 * 355 * @param args Command line args 356 * @return Whether init successful and run should be invoked 357 * @throws ParseException 358 * @throws IOException 359 */ 360 public boolean init(String[] args) throws ParseException, IOException { 361 Options opts = new Options(); 362 opts.addOption("app_attempt_id", true, 363 "App Attempt ID. Not to be used unless for testing purposes"); 364 opts.addOption("shell_env", true, 365 "Environment for shell script. Specified as env_key=env_val pairs"); 366 opts.addOption("container_memory", true, 367 "Amount of memory in MB to be requested to run the shell command"); 368 opts.addOption("container_vcores", true, 369 "Amount of virtual cores to be requested to run the shell command"); 370 opts.addOption("num_containers", true, 371 "No. of containers on which the shell command needs to be executed"); 372 opts.addOption("priority", true, "Application Priority. Default 0"); 373 opts.addOption("debug", false, "Dump out debug information"); 374 375 opts.addOption("help", false, "Print usage"); 376 CommandLine cliParser = new GnuParser().parse(opts, args); 377 378 if (args.length == 0) { 379 printUsage(opts); 380 throw new IllegalArgumentException( 381 "No args specified for application master to initialize"); 382 } 383 384 //Check whether customer log4j.properties file exists 385 if (fileExist(log4jPath)) { 386 try { 387 Log4jPropertyHelper.updateLog4jConfiguration(ApplicationMaster.class, 388 log4jPath); 389 } catch (Exception e) { 390 LOG.warn("Can not set up custom log4j properties. " + e); 391 } 392 } 393 394 if (cliParser.hasOption("help")) { 395 printUsage(opts); 396 return false; 397 } 398 399 if (cliParser.hasOption("debug")) { 400 dumpOutDebugInfo(); 401 } 402 403 Map<String, String> envs = System.getenv(); 404 405 if (!envs.containsKey(Environment.CONTAINER_ID.name())) { 406 if (cliParser.hasOption("app_attempt_id")) { 407 String appIdStr = cliParser.getOptionValue("app_attempt_id", ""); 408 appAttemptID = ConverterUtils.toApplicationAttemptId(appIdStr); 409 } else { 410 throw new IllegalArgumentException( 411 "Application Attempt Id not set in the environment"); 412 } 413 } else { 414 ContainerId containerId = ConverterUtils.toContainerId(envs 415 .get(Environment.CONTAINER_ID.name())); 416 appAttemptID = containerId.getApplicationAttemptId(); 417 } 418 419 if (!envs.containsKey(ApplicationConstants.APP_SUBMIT_TIME_ENV)) { 420 throw new RuntimeException(ApplicationConstants.APP_SUBMIT_TIME_ENV 421 + " not set in the environment"); 422 } 423 if (!envs.containsKey(Environment.NM_HOST.name())) { 424 throw new RuntimeException(Environment.NM_HOST.name() 425 + " not set in the environment"); 426 } 427 if (!envs.containsKey(Environment.NM_HTTP_PORT.name())) { 428 throw new RuntimeException(Environment.NM_HTTP_PORT 429 + " not set in the environment"); 430 } 431 if (!envs.containsKey(Environment.NM_PORT.name())) { 432 throw new RuntimeException(Environment.NM_PORT.name() 433 + " not set in the environment"); 434 } 435 436 LOG.info("Application master for app" + ", appId=" 437 + appAttemptID.getApplicationId().getId() + ", clustertimestamp=" 438 + appAttemptID.getApplicationId().getClusterTimestamp() 439 + ", attemptId=" + appAttemptID.getAttemptId()); 440 441 if (!fileExist(shellCommandPath) 442 && envs.get(DSConstants.DISTRIBUTEDSHELLSCRIPTLOCATION).isEmpty()) { 443 throw new IllegalArgumentException( 444 "No shell command or shell script specified to be executed by application master"); 445 } 446 447 if (fileExist(shellCommandPath)) { 448 shellCommand = readContent(shellCommandPath); 449 } 450 451 if (fileExist(shellArgsPath)) { 452 shellArgs = readContent(shellArgsPath); 453 } 454 455 if (cliParser.hasOption("shell_env")) { 456 String shellEnvs[] = cliParser.getOptionValues("shell_env"); 457 for (String env : shellEnvs) { 458 env = env.trim(); 459 int index = env.indexOf('='); 460 if (index == -1) { 461 shellEnv.put(env, ""); 462 continue; 463 } 464 String key = env.substring(0, index); 465 String val = ""; 466 if (index < (env.length() - 1)) { 467 val = env.substring(index + 1); 468 } 469 shellEnv.put(key, val); 470 } 471 } 472 473 if (envs.containsKey(DSConstants.DISTRIBUTEDSHELLSCRIPTLOCATION)) { 474 scriptPath = envs.get(DSConstants.DISTRIBUTEDSHELLSCRIPTLOCATION); 475 476 if (envs.containsKey(DSConstants.DISTRIBUTEDSHELLSCRIPTTIMESTAMP)) { 477 shellScriptPathTimestamp = Long.valueOf(envs 478 .get(DSConstants.DISTRIBUTEDSHELLSCRIPTTIMESTAMP)); 479 } 480 if (envs.containsKey(DSConstants.DISTRIBUTEDSHELLSCRIPTLEN)) { 481 shellScriptPathLen = Long.valueOf(envs 482 .get(DSConstants.DISTRIBUTEDSHELLSCRIPTLEN)); 483 } 484 if (!scriptPath.isEmpty() 485 && (shellScriptPathTimestamp <= 0 || shellScriptPathLen <= 0)) { 486 LOG.error("Illegal values in env for shell script path" + ", path=" 487 + scriptPath + ", len=" + shellScriptPathLen + ", timestamp=" 488 + shellScriptPathTimestamp); 489 throw new IllegalArgumentException( 490 "Illegal values in env for shell script path"); 491 } 492 } 493 494 if (envs.containsKey(DSConstants.DISTRIBUTEDSHELLTIMELINEDOMAIN)) { 495 domainId = envs.get(DSConstants.DISTRIBUTEDSHELLTIMELINEDOMAIN); 496 } 497 498 containerMemory = Integer.parseInt(cliParser.getOptionValue( 499 "container_memory", "10")); 500 containerVirtualCores = Integer.parseInt(cliParser.getOptionValue( 501 "container_vcores", "1")); 502 numTotalContainers = Integer.parseInt(cliParser.getOptionValue( 503 "num_containers", "1")); 504 if (numTotalContainers == 0) { 505 throw new IllegalArgumentException( 506 "Cannot run distributed shell with no containers"); 507 } 508 requestPriority = Integer.parseInt(cliParser 509 .getOptionValue("priority", "0")); 510 511 // Creating the Timeline Client 512 timelineClient = TimelineClient.createTimelineClient(); 513 timelineClient.init(conf); 514 timelineClient.start(); 515 516 return true; 517 } 518 519 /** 520 * Helper function to print usage 521 * 522 * @param opts Parsed command line options 523 */ 524 private void printUsage(Options opts) { 525 new HelpFormatter().printHelp("ApplicationMaster", opts); 526 } 527 528 /** 529 * Main run function for the application master 530 * 531 * @throws YarnException 532 * @throws IOException 533 */ 534 @SuppressWarnings({ "unchecked" }) 535 public void run() throws YarnException, IOException { 536 LOG.info("Starting ApplicationMaster"); 537 538 // Note: Credentials, Token, UserGroupInformation, DataOutputBuffer class 539 // are marked as LimitedPrivate 540 Credentials credentials = 541 UserGroupInformation.getCurrentUser().getCredentials(); 542 DataOutputBuffer dob = new DataOutputBuffer(); 543 credentials.writeTokenStorageToStream(dob); 544 // Now remove the AM->RM token so that containers cannot access it. 545 Iterator<Token<?>> iter = credentials.getAllTokens().iterator(); 546 LOG.info("Executing with tokens:"); 547 while (iter.hasNext()) { 548 Token<?> token = iter.next(); 549 LOG.info(token); 550 if (token.getKind().equals(AMRMTokenIdentifier.KIND_NAME)) { 551 iter.remove(); 552 } 553 } 554 allTokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength()); 555 556 // Create appSubmitterUgi and add original tokens to it 557 String appSubmitterUserName = 558 System.getenv(ApplicationConstants.Environment.USER.name()); 559 appSubmitterUgi = 560 UserGroupInformation.createRemoteUser(appSubmitterUserName); 561 appSubmitterUgi.addCredentials(credentials); 562 563 publishApplicationAttemptEvent(timelineClient, appAttemptID.toString(), 564 DSEvent.DS_APP_ATTEMPT_START, domainId, appSubmitterUgi); 565 566 AMRMClientAsync.CallbackHandler allocListener = new RMCallbackHandler(); 567 amRMClient = AMRMClientAsync.createAMRMClientAsync(1000, allocListener); 568 amRMClient.init(conf); 569 amRMClient.start(); 570 571 containerListener = createNMCallbackHandler(); 572 nmClientAsync = new NMClientAsyncImpl(containerListener); 573 nmClientAsync.init(conf); 574 nmClientAsync.start(); 575 576 // Setup local RPC Server to accept status requests directly from clients 577 // TODO need to setup a protocol for client to be able to communicate to 578 // the RPC server 579 // TODO use the rpc port info to register with the RM for the client to 580 // send requests to this app master 581 582 // Register self with ResourceManager 583 // This will start heartbeating to the RM 584 appMasterHostname = NetUtils.getHostname(); 585 RegisterApplicationMasterResponse response = amRMClient 586 .registerApplicationMaster(appMasterHostname, appMasterRpcPort, 587 appMasterTrackingUrl); 588 // Dump out information about cluster capability as seen by the 589 // resource manager 590 int maxMem = response.getMaximumResourceCapability().getMemory(); 591 LOG.info("Max mem capabililty of resources in this cluster " + maxMem); 592 593 int maxVCores = response.getMaximumResourceCapability().getVirtualCores(); 594 LOG.info("Max vcores capabililty of resources in this cluster " + maxVCores); 595 596 // A resource ask cannot exceed the max. 597 if (containerMemory > maxMem) { 598 LOG.info("Container memory specified above max threshold of cluster." 599 + " Using max value." + ", specified=" + containerMemory + ", max=" 600 + maxMem); 601 containerMemory = maxMem; 602 } 603 604 if (containerVirtualCores > maxVCores) { 605 LOG.info("Container virtual cores specified above max threshold of cluster." 606 + " Using max value." + ", specified=" + containerVirtualCores + ", max=" 607 + maxVCores); 608 containerVirtualCores = maxVCores; 609 } 610 611 List<Container> previousAMRunningContainers = 612 response.getContainersFromPreviousAttempts(); 613 LOG.info(appAttemptID + " received " + previousAMRunningContainers.size() 614 + " previous attempts' running containers on AM registration."); 615 for(Container container: previousAMRunningContainers) { 616 launchedContainers.add(container.getId()); 617 } 618 numAllocatedContainers.addAndGet(previousAMRunningContainers.size()); 619 620 621 int numTotalContainersToRequest = 622 numTotalContainers - previousAMRunningContainers.size(); 623 // Setup ask for containers from RM 624 // Send request for containers to RM 625 // Until we get our fully allocated quota, we keep on polling RM for 626 // containers 627 // Keep looping until all the containers are launched and shell script 628 // executed on them ( regardless of success/failure). 629 for (int i = 0; i < numTotalContainersToRequest; ++i) { 630 ContainerRequest containerAsk = setupContainerAskForRM(); 631 amRMClient.addContainerRequest(containerAsk); 632 } 633 numRequestedContainers.set(numTotalContainers); 634 635 publishApplicationAttemptEvent(timelineClient, appAttemptID.toString(), 636 DSEvent.DS_APP_ATTEMPT_END, domainId, appSubmitterUgi); 637 } 638 639 @VisibleForTesting 640 NMCallbackHandler createNMCallbackHandler() { 641 return new NMCallbackHandler(this); 642 } 643 644 @VisibleForTesting 645 protected boolean finish() { 646 // wait for completion. 647 while (!done 648 && (numCompletedContainers.get() != numTotalContainers)) { 649 try { 650 Thread.sleep(200); 651 } catch (InterruptedException ex) {} 652 } 653 654 // Join all launched threads 655 // needed for when we time out 656 // and we need to release containers 657 for (Thread launchThread : launchThreads) { 658 try { 659 launchThread.join(10000); 660 } catch (InterruptedException e) { 661 LOG.info("Exception thrown in thread join: " + e.getMessage()); 662 e.printStackTrace(); 663 } 664 } 665 666 // When the application completes, it should stop all running containers 667 LOG.info("Application completed. Stopping running containers"); 668 nmClientAsync.stop(); 669 670 // When the application completes, it should send a finish application 671 // signal to the RM 672 LOG.info("Application completed. Signalling finish to RM"); 673 674 FinalApplicationStatus appStatus; 675 String appMessage = null; 676 boolean success = true; 677 if (numFailedContainers.get() == 0 && 678 numCompletedContainers.get() == numTotalContainers) { 679 appStatus = FinalApplicationStatus.SUCCEEDED; 680 } else { 681 appStatus = FinalApplicationStatus.FAILED; 682 appMessage = "Diagnostics." + ", total=" + numTotalContainers 683 + ", completed=" + numCompletedContainers.get() + ", allocated=" 684 + numAllocatedContainers.get() + ", failed=" 685 + numFailedContainers.get(); 686 LOG.info(appMessage); 687 success = false; 688 } 689 try { 690 amRMClient.unregisterApplicationMaster(appStatus, appMessage, null); 691 } catch (YarnException ex) { 692 LOG.error("Failed to unregister application", ex); 693 } catch (IOException e) { 694 LOG.error("Failed to unregister application", e); 695 } 696 697 amRMClient.stop(); 698 699 return success; 700 } 701 702 @VisibleForTesting 703 class RMCallbackHandler implements AMRMClientAsync.CallbackHandler { 704 @SuppressWarnings("unchecked") 705 @Override 706 public void onContainersCompleted(List<ContainerStatus> completedContainers) { 707 LOG.info("Got response from RM for container ask, completedCnt=" 708 + completedContainers.size()); 709 for (ContainerStatus containerStatus : completedContainers) { 710 LOG.info(appAttemptID + " got container status for containerID=" 711 + containerStatus.getContainerId() + ", state=" 712 + containerStatus.getState() + ", exitStatus=" 713 + containerStatus.getExitStatus() + ", diagnostics=" 714 + containerStatus.getDiagnostics()); 715 716 // non complete containers should not be here 717 assert (containerStatus.getState() == ContainerState.COMPLETE); 718 // ignore containers we know nothing about - probably from a previous 719 // attempt 720 if (!launchedContainers.contains(containerStatus.getContainerId())) { 721 LOG.info("Ignoring completed status of " 722 + containerStatus.getContainerId() 723 + "; unknown container(probably launched by previous attempt)"); 724 continue; 725 } 726 727 // increment counters for completed/failed containers 728 int exitStatus = containerStatus.getExitStatus(); 729 if (0 != exitStatus) { 730 // container failed 731 if (ContainerExitStatus.ABORTED != exitStatus) { 732 // shell script failed 733 // counts as completed 734 numCompletedContainers.incrementAndGet(); 735 numFailedContainers.incrementAndGet(); 736 } else { 737 // container was killed by framework, possibly preempted 738 // we should re-try as the container was lost for some reason 739 numAllocatedContainers.decrementAndGet(); 740 numRequestedContainers.decrementAndGet(); 741 // we do not need to release the container as it would be done 742 // by the RM 743 } 744 } else { 745 // nothing to do 746 // container completed successfully 747 numCompletedContainers.incrementAndGet(); 748 LOG.info("Container completed successfully." + ", containerId=" 749 + containerStatus.getContainerId()); 750 } 751 publishContainerEndEvent( 752 timelineClient, containerStatus, domainId, appSubmitterUgi); 753 } 754 755 // ask for more containers if any failed 756 int askCount = numTotalContainers - numRequestedContainers.get(); 757 numRequestedContainers.addAndGet(askCount); 758 759 if (askCount > 0) { 760 for (int i = 0; i < askCount; ++i) { 761 ContainerRequest containerAsk = setupContainerAskForRM(); 762 amRMClient.addContainerRequest(containerAsk); 763 } 764 } 765 766 if (numCompletedContainers.get() == numTotalContainers) { 767 done = true; 768 } 769 } 770 771 @Override 772 public void onContainersAllocated(List<Container> allocatedContainers) { 773 LOG.info("Got response from RM for container ask, allocatedCnt=" 774 + allocatedContainers.size()); 775 numAllocatedContainers.addAndGet(allocatedContainers.size()); 776 for (Container allocatedContainer : allocatedContainers) { 777 String yarnShellId = Integer.toString(yarnShellIdCounter); 778 yarnShellIdCounter++; 779 LOG.info("Launching shell command on a new container." 780 + ", containerId=" + allocatedContainer.getId() 781 + ", yarnShellId=" + yarnShellId 782 + ", containerNode=" + allocatedContainer.getNodeId().getHost() 783 + ":" + allocatedContainer.getNodeId().getPort() 784 + ", containerNodeURI=" + allocatedContainer.getNodeHttpAddress() 785 + ", containerResourceMemory" 786 + allocatedContainer.getResource().getMemory() 787 + ", containerResourceVirtualCores" 788 + allocatedContainer.getResource().getVirtualCores()); 789 // + ", containerToken" 790 // +allocatedContainer.getContainerToken().getIdentifier().toString()); 791 792 Thread launchThread = createLaunchContainerThread(allocatedContainer, 793 yarnShellId); 794 795 // launch and start the container on a separate thread to keep 796 // the main thread unblocked 797 // as all containers may not be allocated at one go. 798 launchThreads.add(launchThread); 799 launchedContainers.add(allocatedContainer.getId()); 800 launchThread.start(); 801 } 802 } 803 804 @Override 805 public void onShutdownRequest() { 806 done = true; 807 } 808 809 @Override 810 public void onNodesUpdated(List<NodeReport> updatedNodes) {} 811 812 @Override 813 public float getProgress() { 814 // set progress to deliver to RM on next heartbeat 815 float progress = (float) numCompletedContainers.get() 816 / numTotalContainers; 817 return progress; 818 } 819 820 @Override 821 public void onError(Throwable e) { 822 done = true; 823 amRMClient.stop(); 824 } 825 } 826 827 @VisibleForTesting 828 static class NMCallbackHandler 829 implements NMClientAsync.CallbackHandler { 830 831 private ConcurrentMap<ContainerId, Container> containers = 832 new ConcurrentHashMap<ContainerId, Container>(); 833 private final ApplicationMaster applicationMaster; 834 835 public NMCallbackHandler(ApplicationMaster applicationMaster) { 836 this.applicationMaster = applicationMaster; 837 } 838 839 public void addContainer(ContainerId containerId, Container container) { 840 containers.putIfAbsent(containerId, container); 841 } 842 843 @Override 844 public void onContainerStopped(ContainerId containerId) { 845 if (LOG.isDebugEnabled()) { 846 LOG.debug("Succeeded to stop Container " + containerId); 847 } 848 containers.remove(containerId); 849 } 850 851 @Override 852 public void onContainerStatusReceived(ContainerId containerId, 853 ContainerStatus containerStatus) { 854 if (LOG.isDebugEnabled()) { 855 LOG.debug("Container Status: id=" + containerId + ", status=" + 856 containerStatus); 857 } 858 } 859 860 @Override 861 public void onContainerStarted(ContainerId containerId, 862 Map<String, ByteBuffer> allServiceResponse) { 863 if (LOG.isDebugEnabled()) { 864 LOG.debug("Succeeded to start Container " + containerId); 865 } 866 Container container = containers.get(containerId); 867 if (container != null) { 868 applicationMaster.nmClientAsync.getContainerStatusAsync(containerId, container.getNodeId()); 869 } 870 ApplicationMaster.publishContainerStartEvent( 871 applicationMaster.timelineClient, container, 872 applicationMaster.domainId, applicationMaster.appSubmitterUgi); 873 } 874 875 @Override 876 public void onStartContainerError(ContainerId containerId, Throwable t) { 877 LOG.error("Failed to start Container " + containerId); 878 containers.remove(containerId); 879 applicationMaster.numCompletedContainers.incrementAndGet(); 880 applicationMaster.numFailedContainers.incrementAndGet(); 881 } 882 883 @Override 884 public void onGetContainerStatusError( 885 ContainerId containerId, Throwable t) { 886 LOG.error("Failed to query the status of Container " + containerId); 887 } 888 889 @Override 890 public void onStopContainerError(ContainerId containerId, Throwable t) { 891 LOG.error("Failed to stop Container " + containerId); 892 containers.remove(containerId); 893 } 894 } 895 896 /** 897 * Thread to connect to the {@link ContainerManagementProtocol} and launch the container 898 * that will execute the shell command. 899 */ 900 private class LaunchContainerRunnable implements Runnable { 901 902 // Allocated container 903 private Container container; 904 private String shellId; 905 906 NMCallbackHandler containerListener; 907 908 /** 909 * @param lcontainer Allocated container 910 * @param containerListener Callback handler of the container 911 */ 912 public LaunchContainerRunnable(Container lcontainer, 913 NMCallbackHandler containerListener, String shellId) { 914 this.container = lcontainer; 915 this.containerListener = containerListener; 916 this.shellId = shellId; 917 } 918 919 @Override 920 /** 921 * Connects to CM, sets up container launch context 922 * for shell command and eventually dispatches the container 923 * start request to the CM. 924 */ 925 public void run() { 926 LOG.info("Setting up container launch container for containerid=" 927 + container.getId() + " with shellid=" + shellId); 928 929 // Set the local resources 930 Map<String, LocalResource> localResources = new HashMap<String, LocalResource>(); 931 932 // The container for the eventual shell commands needs its own local 933 // resources too. 934 // In this scenario, if a shell script is specified, we need to have it 935 // copied and made available to the container. 936 if (!scriptPath.isEmpty()) { 937 Path renamedScriptPath = null; 938 if (Shell.WINDOWS) { 939 renamedScriptPath = new Path(scriptPath + ".bat"); 940 } else { 941 renamedScriptPath = new Path(scriptPath + ".sh"); 942 } 943 944 try { 945 // rename the script file based on the underlying OS syntax. 946 renameScriptFile(renamedScriptPath); 947 } catch (Exception e) { 948 LOG.error( 949 "Not able to add suffix (.bat/.sh) to the shell script filename", 950 e); 951 // We know we cannot continue launching the container 952 // so we should release it. 953 numCompletedContainers.incrementAndGet(); 954 numFailedContainers.incrementAndGet(); 955 return; 956 } 957 958 URL yarnUrl = null; 959 try { 960 yarnUrl = ConverterUtils.getYarnUrlFromURI( 961 new URI(renamedScriptPath.toString())); 962 } catch (URISyntaxException e) { 963 LOG.error("Error when trying to use shell script path specified" 964 + " in env, path=" + renamedScriptPath, e); 965 // A failure scenario on bad input such as invalid shell script path 966 // We know we cannot continue launching the container 967 // so we should release it. 968 // TODO 969 numCompletedContainers.incrementAndGet(); 970 numFailedContainers.incrementAndGet(); 971 return; 972 } 973 LocalResource shellRsrc = LocalResource.newInstance(yarnUrl, 974 LocalResourceType.FILE, LocalResourceVisibility.APPLICATION, 975 shellScriptPathLen, shellScriptPathTimestamp); 976 localResources.put(Shell.WINDOWS ? ExecBatScripStringtPath : 977 ExecShellStringPath, shellRsrc); 978 shellCommand = Shell.WINDOWS ? windows_command : linux_bash_command; 979 } 980 981 // Set the necessary command to execute on the allocated container 982 Vector<CharSequence> vargs = new Vector<CharSequence>(5); 983 984 // Set executable command 985 vargs.add(shellCommand); 986 // Set shell script path 987 if (!scriptPath.isEmpty()) { 988 vargs.add(Shell.WINDOWS ? ExecBatScripStringtPath 989 : ExecShellStringPath); 990 } 991 992 // Set args for the shell command if any 993 vargs.add(shellArgs); 994 // Add log redirect params 995 vargs.add("1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout"); 996 vargs.add("2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr"); 997 998 // Get final commmand 999 StringBuilder command = new StringBuilder(); 1000 for (CharSequence str : vargs) { 1001 command.append(str).append(" "); 1002 } 1003 1004 List<String> commands = new ArrayList<String>(); 1005 commands.add(command.toString()); 1006 1007 // Set up ContainerLaunchContext, setting local resource, environment, 1008 // command and token for constructor. 1009 1010 // Note for tokens: Set up tokens for the container too. Today, for normal 1011 // shell commands, the container in distribute-shell doesn't need any 1012 // tokens. We are populating them mainly for NodeManagers to be able to 1013 // download anyfiles in the distributed file-system. The tokens are 1014 // otherwise also useful in cases, for e.g., when one is running a 1015 // "hadoop dfs" command inside the distributed shell. 1016 Map<String, String> myShellEnv = new HashMap<String, String>(shellEnv); 1017 myShellEnv.put(YARN_SHELL_ID, shellId); 1018 ContainerLaunchContext ctx = ContainerLaunchContext.newInstance( 1019 localResources, myShellEnv, commands, null, allTokens.duplicate(), 1020 null); 1021 containerListener.addContainer(container.getId(), container); 1022 nmClientAsync.startContainerAsync(container, ctx); 1023 } 1024 } 1025 1026 private void renameScriptFile(final Path renamedScriptPath) 1027 throws IOException, InterruptedException { 1028 appSubmitterUgi.doAs(new PrivilegedExceptionAction<Void>() { 1029 @Override 1030 public Void run() throws IOException { 1031 FileSystem fs = renamedScriptPath.getFileSystem(conf); 1032 fs.rename(new Path(scriptPath), renamedScriptPath); 1033 return null; 1034 } 1035 }); 1036 LOG.info("User " + appSubmitterUgi.getUserName() 1037 + " added suffix(.sh/.bat) to script file as " + renamedScriptPath); 1038 } 1039 1040 /** 1041 * Setup the request that will be sent to the RM for the container ask. 1042 * 1043 * @return the setup ResourceRequest to be sent to RM 1044 */ 1045 private ContainerRequest setupContainerAskForRM() { 1046 // setup requirements for hosts 1047 // using * as any host will do for the distributed shell app 1048 // set the priority for the request 1049 // TODO - what is the range for priority? how to decide? 1050 Priority pri = Priority.newInstance(requestPriority); 1051 1052 // Set up resource type requirements 1053 // For now, memory and CPU are supported so we set memory and cpu requirements 1054 Resource capability = Resource.newInstance(containerMemory, 1055 containerVirtualCores); 1056 1057 ContainerRequest request = new ContainerRequest(capability, null, null, 1058 pri); 1059 LOG.info("Requested container ask: " + request.toString()); 1060 return request; 1061 } 1062 1063 private boolean fileExist(String filePath) { 1064 return new File(filePath).exists(); 1065 } 1066 1067 private String readContent(String filePath) throws IOException { 1068 DataInputStream ds = null; 1069 try { 1070 ds = new DataInputStream(new FileInputStream(filePath)); 1071 return ds.readUTF(); 1072 } finally { 1073 org.apache.commons.io.IOUtils.closeQuietly(ds); 1074 } 1075 } 1076 1077 private static void publishContainerStartEvent( 1078 final TimelineClient timelineClient, Container container, String domainId, 1079 UserGroupInformation ugi) { 1080 final TimelineEntity entity = new TimelineEntity(); 1081 entity.setEntityId(container.getId().toString()); 1082 entity.setEntityType(DSEntity.DS_CONTAINER.toString()); 1083 entity.setDomainId(domainId); 1084 entity.addPrimaryFilter("user", ugi.getShortUserName()); 1085 TimelineEvent event = new TimelineEvent(); 1086 event.setTimestamp(System.currentTimeMillis()); 1087 event.setEventType(DSEvent.DS_CONTAINER_START.toString()); 1088 event.addEventInfo("Node", container.getNodeId().toString()); 1089 event.addEventInfo("Resources", container.getResource().toString()); 1090 entity.addEvent(event); 1091 1092 try { 1093 ugi.doAs(new PrivilegedExceptionAction<TimelinePutResponse>() { 1094 @Override 1095 public TimelinePutResponse run() throws Exception { 1096 return timelineClient.putEntities(entity); 1097 } 1098 }); 1099 } catch (Exception e) { 1100 LOG.error("Container start event could not be published for " 1101 + container.getId().toString(), 1102 e instanceof UndeclaredThrowableException ? e.getCause() : e); 1103 } 1104 } 1105 1106 private static void publishContainerEndEvent( 1107 final TimelineClient timelineClient, ContainerStatus container, 1108 String domainId, UserGroupInformation ugi) { 1109 final TimelineEntity entity = new TimelineEntity(); 1110 entity.setEntityId(container.getContainerId().toString()); 1111 entity.setEntityType(DSEntity.DS_CONTAINER.toString()); 1112 entity.setDomainId(domainId); 1113 entity.addPrimaryFilter("user", ugi.getShortUserName()); 1114 TimelineEvent event = new TimelineEvent(); 1115 event.setTimestamp(System.currentTimeMillis()); 1116 event.setEventType(DSEvent.DS_CONTAINER_END.toString()); 1117 event.addEventInfo("State", container.getState().name()); 1118 event.addEventInfo("Exit Status", container.getExitStatus()); 1119 entity.addEvent(event); 1120 1121 try { 1122 ugi.doAs(new PrivilegedExceptionAction<TimelinePutResponse>() { 1123 @Override 1124 public TimelinePutResponse run() throws Exception { 1125 return timelineClient.putEntities(entity); 1126 } 1127 }); 1128 } catch (Exception e) { 1129 LOG.error("Container end event could not be published for " 1130 + container.getContainerId().toString(), 1131 e instanceof UndeclaredThrowableException ? e.getCause() : e); 1132 } 1133 } 1134 1135 private static void publishApplicationAttemptEvent( 1136 final TimelineClient timelineClient, String appAttemptId, 1137 DSEvent appEvent, String domainId, UserGroupInformation ugi) { 1138 final TimelineEntity entity = new TimelineEntity(); 1139 entity.setEntityId(appAttemptId); 1140 entity.setEntityType(DSEntity.DS_APP_ATTEMPT.toString()); 1141 entity.setDomainId(domainId); 1142 entity.addPrimaryFilter("user", ugi.getShortUserName()); 1143 TimelineEvent event = new TimelineEvent(); 1144 event.setEventType(appEvent.toString()); 1145 event.setTimestamp(System.currentTimeMillis()); 1146 entity.addEvent(event); 1147 1148 try { 1149 ugi.doAs(new PrivilegedExceptionAction<TimelinePutResponse>() { 1150 @Override 1151 public TimelinePutResponse run() throws Exception { 1152 return timelineClient.putEntities(entity); 1153 } 1154 }); 1155 } catch (Exception e) { 1156 LOG.error("App Attempt " 1157 + (appEvent.equals(DSEvent.DS_APP_ATTEMPT_START) ? "start" : "end") 1158 + " event could not be published for " 1159 + appAttemptId.toString(), 1160 e instanceof UndeclaredThrowableException ? e.getCause() : e); 1161 } 1162 } 1163 1164 RMCallbackHandler getRMCallbackHandler() { 1165 return new RMCallbackHandler(); 1166 } 1167 1168 @VisibleForTesting 1169 void setAmRMClient(AMRMClientAsync client) { 1170 this.amRMClient = client; 1171 } 1172 1173 @VisibleForTesting 1174 int getNumCompletedContainers() { 1175 return numCompletedContainers.get(); 1176 } 1177 1178 @VisibleForTesting 1179 boolean getDone() { 1180 return done; 1181 } 1182 1183 @VisibleForTesting 1184 Thread createLaunchContainerThread(Container allocatedContainer, 1185 String shellId) { 1186 LaunchContainerRunnable runnableLaunchContainer = 1187 new LaunchContainerRunnable(allocatedContainer, containerListener, 1188 shellId); 1189 return new Thread(runnableLaunchContainer); 1190 } 1191}