/* * Copyright © 2015 Cask Data, Inc. * * Licensed 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 co.cask.cdap.app.runtime; import co.cask.cdap.api.app.ApplicationSpecification; import co.cask.cdap.app.program.Program; import co.cask.cdap.common.app.RunIds; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.utils.Tasks; import co.cask.cdap.internal.app.runtime.ProgramControllerServiceAdapter; import co.cask.cdap.internal.app.runtime.SimpleProgramOptions; import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.ProgramLiveInfo; import co.cask.cdap.proto.ProgramType; import com.google.common.util.concurrent.AbstractExecutionThreadService; import com.google.common.util.concurrent.AbstractIdleService; import com.google.common.util.concurrent.Service; import org.apache.twill.api.RunId; import org.apache.twill.filesystem.Location; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; /** * Unit-test for AbstractProgramRuntimeService base functionalities. */ public class AbstractProgramRuntimeServiceTest { @ClassRule public static final TemporaryFolder TEMP_FOLDER = new TemporaryFolder(); @Test (timeout = 5000) public void testDeadlock() throws IOException, ExecutionException, InterruptedException, TimeoutException { // This test is for testing condition in (CDAP-3579) // The race condition is if a program finished very fast such that inside the AbstractProgramRuntimeService is // still in the run method, it holds the object lock, making the callback from the listener block forever. ProgramRunnerFactory runnerFactory = createProgramRunnerFactory(); ProgramRuntimeService runtimeService = new AbstractProgramRuntimeService(CConfiguration.create(), runnerFactory, null) { @Override public ProgramLiveInfo getLiveInfo(Id.Program programId) { return new ProgramLiveInfo(programId, "runtime") { }; } }; runtimeService.startAndWait(); try { Program program = createDummyProgram(); final ProgramController controller = runtimeService.run(program, new SimpleProgramOptions(program)).getController(); Tasks.waitFor(ProgramController.State.COMPLETED, new Callable<ProgramController.State>() { @Override public ProgramController.State call() throws Exception { return controller.getState(); } }, 5, TimeUnit.SECONDS, 100, TimeUnit.MILLISECONDS); Assert.assertTrue(runtimeService.list(ProgramType.WORKER).isEmpty()); } finally { runtimeService.stopAndWait(); } } @Test (timeout = 5000L) public void testUpdateDeadLock() { // This test is for testing (CDAP-3716) // Create a service to simulate an existing running app. Service service = new TestService(); Id.Program programId = Id.Program.from(Id.Namespace.DEFAULT, "dummyApp", ProgramType.WORKER, "dummy"); RunId runId = RunIds.generate(); ProgramRuntimeService.RuntimeInfo extraInfo = createRuntimeInfo(service, programId, runId); service.startAndWait(); ProgramRunnerFactory runnerFactory = createProgramRunnerFactory(); TestProgramRuntimeService runtimeService = new TestProgramRuntimeService(CConfiguration.create(), runnerFactory, null, extraInfo); runtimeService.startAndWait(); // The lookup will get deadlock for CDAP-3716 Assert.assertNotNull(runtimeService.lookup(programId, runId)); service.stopAndWait(); runtimeService.stopAndWait(); } private ProgramRunnerFactory createProgramRunnerFactory() { return new ProgramRunnerFactory() { @Override public ProgramRunner create(ProgramType programType) { return new ProgramRunner() { @Override public ProgramController run(Program program, ProgramOptions options) { Service service = new FastService(); ProgramController controller = new ProgramControllerServiceAdapter(service, program.getId(), RunIds.generate()); service.start(); return controller; } }; } }; } private Program createDummyProgram() throws IOException { return new Program() { @Override public String getMainClassName() { return null; } @Override public <T> Class<T> getMainClass() throws ClassNotFoundException { return null; } @Override public ProgramType getType() { return ProgramType.WORKER; } @Override public Id.Program getId() { return Id.Program.from(Id.Namespace.DEFAULT, "dummyApp", ProgramType.WORKER, "dummy"); } @Override public String getName() { return getId().getId(); } @Override public String getNamespaceId() { return getId().getNamespaceId(); } @Override public String getApplicationId() { return getId().getApplicationId(); } @Override public ApplicationSpecification getApplicationSpecification() { return null; } @Override public Location getJarLocation() { return null; } @Override public ClassLoader getClassLoader() { return getClass().getClassLoader(); } @Override public void close() throws IOException { // No-op } }; } private ProgramRuntimeService.RuntimeInfo createRuntimeInfo(Service service, final Id.Program programId, RunId runId) { final ProgramControllerServiceAdapter controller = new ProgramControllerServiceAdapter(service, programId, runId); return new ProgramRuntimeService.RuntimeInfo() { @Override public ProgramController getController() { return controller; } @Override public ProgramType getType() { return programId.getType(); } @Override public Id.Program getProgramId() { return programId; } @Nullable @Override public RunId getTwillRunId() { return null; } }; } /** * A guava service that has wil be completed immediate right after it is started. */ private static final class FastService extends AbstractExecutionThreadService { @Override protected void run() throws Exception { // No-op } } /** * A guava service that will not terminate until stop is called. */ private static final class TestService extends AbstractIdleService { @Override protected void startUp() throws Exception { // no-op } @Override protected void shutDown() throws Exception { // no-op } } /** * A runtime service that will insert an extra RuntimeInfo for the lookup call to simulate the situation * where an app is already running when the service starts. */ private static final class TestProgramRuntimeService extends AbstractProgramRuntimeService { private final RuntimeInfo extraInfo; protected TestProgramRuntimeService(CConfiguration cConf, ProgramRunnerFactory programRunnerFactory, @Nullable ArtifactRepository artifactRepository, RuntimeInfo extraInfo) { super(cConf, programRunnerFactory, artifactRepository); this.extraInfo = extraInfo; } @Override public ProgramLiveInfo getLiveInfo(Id.Program programId) { return new ProgramLiveInfo(programId, "runtime") { }; } @Override public RuntimeInfo lookup(Id.Program programId, RunId runId) { RuntimeInfo info = super.lookup(programId, runId); if (info != null) { return info; } updateRuntimeInfo(programId.getType(), runId, extraInfo); return extraInfo; } } }