[ https://issues.apache.org/jira/browse/TWILL-240?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16119076#comment-16119076 ]
ASF GitHub Bot commented on TWILL-240: -------------------------------------- Github user chtyim commented on a diff in the pull request: https://github.com/apache/twill/pull/58#discussion_r132040768 --- Diff: twill-yarn/src/test/java/org/apache/twill/yarn/EventHandlerTest.java --- @@ -0,0 +1,351 @@ +/* + * 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.twill.yarn; + +import com.google.common.base.Joiner; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import org.apache.twill.api.AbstractTwillRunnable; +import org.apache.twill.api.EventHandler; +import org.apache.twill.api.EventHandlerContext; +import org.apache.twill.api.TwillApplication; +import org.apache.twill.api.TwillController; +import org.apache.twill.api.TwillSpecification; +import org.apache.twill.api.logging.PrinterLogHandler; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Tests {@link EventHandler} methods + */ +public final class EventHandlerTest extends BaseYarnTest { + private static final Logger LOG = LoggerFactory.getLogger(EventHandlerTest.class); + + @ClassRule + public static final TemporaryFolder TMP_FOLDER = new TemporaryFolder(); + public static final String STARTED_FILE = "started_file"; + public static final String RUN_FILE = "run_file"; + public static final String CONTAINER_LAUNCHED_FOLDER = "launched_folder"; + public static final String CONTAINER_STOPPED_FOLDER = "stopped_folder"; + public static final String COMPLETED_FILE = "completed_file"; + public static final String KILLED_FILE = "killed_file"; + public static final String ABORTED_FILE = "aborted_file"; + + @Test + public void testComplete() throws InterruptedException, ExecutionException, TimeoutException, IOException { + // Create a parent folder to be written by EventHandler + File parentFolder = TMP_FOLDER.newFolder(); + parentFolder.setWritable(true, false); + TwillController controller = getTwillRunner().prepare(new CompleteApplication(parentFolder.getAbsolutePath())) + .addLogHandler(new PrinterLogHandler(new PrintWriter(System.out, true))) + .start(); + + // Wait for the app to complete within 120 seconds. + try { + controller.awaitTerminated(120, TimeUnit.SECONDS); + Set<String> handlerFiles = new HashSet<>(Arrays.asList(parentFolder.list())); + Assert.assertEquals(5, handlerFiles.size()); + // EventHandler#started() method should be called to create a file + Assert.assertTrue(handlerFiles.contains(STARTED_FILE)); + // CompleteRunnable#run() method should be called to create a file after EventHandler#started() method is called + Assert.assertTrue(handlerFiles.contains(RUN_FILE)); + // EventHandler#containerLaunched(String, int, String) method should be called to create a folder + Assert.assertTrue(handlerFiles.contains(CONTAINER_LAUNCHED_FOLDER)); + // EventHandler#containerStopped(String, int, String, int) method should be called to create a folder + Assert.assertTrue(handlerFiles.contains(CONTAINER_STOPPED_FOLDER)); + // Assert that containerLaunched and containerStopped are called for the same containers + // for the same number of times + String[] containerLaunchedFiles = new File(parentFolder.getAbsolutePath(), CONTAINER_LAUNCHED_FOLDER).list(); + String[] containerStoppedFiles = new File(parentFolder.getAbsolutePath(), CONTAINER_STOPPED_FOLDER).list(); + Assert.assertEquals(containerLaunchedFiles.length, containerStoppedFiles.length); + Assert.assertTrue(Arrays.asList(containerLaunchedFiles).containsAll(Arrays.asList(containerStoppedFiles))); + // EventHandler#completed() method should be called to create a file + Assert.assertTrue(handlerFiles.contains(COMPLETED_FILE)); + } catch (Exception e) { + // kill the app as cleanup + controller.kill(); + } + } + + @Test + public void testKilled() throws IOException, InterruptedException, TimeoutException, ExecutionException { + // Create a parent folder to be written by EventHandler + File parentFolder = TMP_FOLDER.newFolder(); + parentFolder.setWritable(true, false); + TwillController controller = getTwillRunner().prepare(new SleepApplication(parentFolder.getAbsolutePath())) + .addLogHandler(new PrinterLogHandler(new PrintWriter(System.out, true))) + .start(); + try { + // Wait for the runnable to run and create runFile within 120 secs + File runFile = new File(parentFolder, RUN_FILE); + Stopwatch stopwatch = new Stopwatch().start(); + while (!runFile.exists() && stopwatch.elapsedTime(TimeUnit.SECONDS) < 120) { + TimeUnit.SECONDS.sleep(1); + } + Assert.assertTrue(runFile.exists()); + // Terminate the app once the runnable runs + controller.terminate(); + controller.awaitTerminated(120, TimeUnit.SECONDS); + Set<String> handlerFiles = new HashSet<>(Arrays.asList(parentFolder.list())); + // EventHandler#killed() method should be called to create a file + Assert.assertTrue(handlerFiles.contains(KILLED_FILE)); + // EventHandler#completed() method should not be called + Assert.assertFalse(handlerFiles.contains(COMPLETED_FILE)); + // EventHandler#aborted() method should not be called + Assert.assertFalse(handlerFiles.contains(ABORTED_FILE)); + } catch (Exception e) { + // kill the app as cleanup + controller.kill(); + } + } + + /** + * The handler for testing timeout handling. + */ + public static final class Handler extends EventHandler { + + private final String parentFolderPath; + private boolean abort; + + public Handler(String parentFolderPath) { + this.parentFolderPath = parentFolderPath; + } + + @Override + protected Map<String, String> getConfigs() { + return ImmutableMap.<String, String>builder() + .put("abort", "true") + .put("parentFolderPath", parentFolderPath) + .put("startedFile", STARTED_FILE) + .put("runFile", RUN_FILE) + .put("containerLaunched", CONTAINER_LAUNCHED_FOLDER) + .put("containerStopped", CONTAINER_STOPPED_FOLDER) + .put("completedFile", COMPLETED_FILE) + .put("killedFile", KILLED_FILE) + .build(); + } + + @Override + public void initialize(EventHandlerContext context) { + super.initialize(context); + this.abort = Boolean.parseBoolean(context.getSpecification().getConfigs().get("abort")); + } + + @Override + public TimeoutAction launchTimeout(Iterable<TimeoutEvent> timeoutEvents) { + if (abort) { + return TimeoutAction.abort(); + } else { + return TimeoutAction.recheck(10, TimeUnit.SECONDS); + } + } + + @Override + public void started() { + try { + new File(context.getSpecification().getConfigs().get("parentFolderPath"), + context.getSpecification().getConfigs().get("startedFile")).createNewFile(); + } catch (IOException e) { + Throwables.propagate(e); + } + } + + @Override + public void containerLaunched(String runnableName, int instanceId, String containerId) { + LOG.info("Launched {}#{} in container {}", runnableName, instanceId, containerId); + createContainerFile(runnableName, instanceId, containerId, "containerLaunched"); + } + + @Override + public void containerStopped(String runnableName, int instanceId, String containerId, int exitStatus) { + LOG.info("Stopped {}#{} in container {} with status {}", runnableName, instanceId, containerId, exitStatus); + createContainerFile(runnableName, instanceId, containerId, "containerStopped"); + } + + private void createContainerFile(String runnableName, int instanceId, String containerId, String folderKey) { + Map<String, String> configs = context.getSpecification().getConfigs(); + File launchedFolder = new File(configs.get("parentFolderPath"), configs.get(folderKey)); + if (!launchedFolder.exists()) { + launchedFolder.mkdirs(); + launchedFolder.setReadable(true, false); + } + try { + new File(launchedFolder.getAbsolutePath(), Joiner.on(":").join(runnableName, instanceId, containerId)) --- End diff -- Would be clear if you have a private `touch(String name)` method rather than have `try-catch` block on each callback method. > Improve EventHandler to handle more application lifecycle events > ---------------------------------------------------------------- > > Key: TWILL-240 > URL: https://issues.apache.org/jira/browse/TWILL-240 > Project: Apache Twill > Issue Type: New Feature > Reporter: Chengfeng Mao > Assignee: Chengfeng Mao > Fix For: 0.12.0 > > > Application Master should be able to run application specific code when > certain lifecycle events happen by calling methods from EventHandler. For > instance, when the app first starts, completes, aborts and etc. -- This message was sent by Atlassian JIRA (v6.4.14#64029)