/*
* 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.brooklyn.entity.software.base;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.ImplementedBy;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.mgmt.EntityManager;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.TaskAdaptable;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.drivers.BasicEntityDriverManager;
import org.apache.brooklyn.core.entity.drivers.ReflectiveEntityDriverFactory;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.location.Locations;
import org.apache.brooklyn.core.location.PortRanges;
import org.apache.brooklyn.core.location.SimulatedLocation;
import org.apache.brooklyn.core.sensor.PortAttributeSensorAndConfigKey;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessDriver;
import org.apache.brooklyn.entity.software.base.EmptySoftwareProcessImpl;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.entity.software.base.SoftwareProcessDriver;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters.RestartMachineMode;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasksTest;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.test.EntityTestUtils;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException;
import org.apache.brooklyn.util.net.UserAndHostAndPort;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.jclouds.util.Throwables2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.brooklyn.location.byon.FixedListMachineProvisioningLocation;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
public class SoftwareProcessEntityTest extends BrooklynAppUnitTestSupport {
// NB: These tests don't actually require ssh to localhost -- only that 'localhost' resolves.
private static final Logger LOG = LoggerFactory.getLogger(SoftwareProcessEntityTest.class);
private SshMachineLocation machine;
private FixedListMachineProvisioningLocation<SshMachineLocation> loc;
@BeforeMethod(alwaysRun=true)
@Override
public void setUp() throws Exception {
super.setUp();
loc = getLocation();
}
@SuppressWarnings("unchecked")
private FixedListMachineProvisioningLocation<SshMachineLocation> getLocation() {
FixedListMachineProvisioningLocation<SshMachineLocation> loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class));
machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure("address", "localhost"));
loc.addMachine(machine);
return loc;
}
@Test
public void testSetsMachineAttributes() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
assertEquals(entity.getAttribute(SoftwareProcess.HOSTNAME), machine.getAddress().getHostName());
assertEquals(entity.getAttribute(SoftwareProcess.ADDRESS), machine.getAddress().getHostAddress());
assertEquals(entity.getAttribute(Attributes.SSH_ADDRESS), UserAndHostAndPort.fromParts(machine.getUser(), machine.getAddress().getHostName(), machine.getPort()));
assertEquals(entity.getAttribute(SoftwareProcess.PROVISIONING_LOCATION), loc);
}
@Test
public void testRequiredPortsFromConfig() throws Exception {
// getRequiredOpenPorts() should only consider numeric and string config whose names end in '.port'
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
.configure("foo.number.port", 1111)
.configure("foo.string.port", "2222")
.configure("foo.number.somethingElse", 3333)
.configure("foo.string.somethingElse", "4444")
);
entity.start(ImmutableList.of(loc));
Collection<Integer> requiredPorts = entity.getRequiredOpenPorts();
assertTrue(requiredPorts.contains(1111));
assertTrue(requiredPorts.contains(2222));
assertFalse(requiredPorts.contains(3333));
assertFalse(requiredPorts.contains(4444));
}
@Test
public void testProcessTemplateWithExtraSubstitutions() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver driver = (SimulatedDriver) entity.getDriver();
Map<String,String> substitutions = MutableMap.of("myname","peter");
String result = driver.processTemplate("/org/apache/brooklyn/entity/software/base/template_with_extra_substitutions.txt",substitutions);
Assert.assertTrue(result.contains("peter"));
}
@Test
public void testInstallDirAndRunDir() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
.configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo"));
entity.start(ImmutableList.of(loc));
Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/MyService");
Assert.assertEquals(entity.getAttribute(SoftwareProcess.RUN_DIR), "/tmp/brooklyn-foo/apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId());
}
@Test
public void testInstallDirAndRunDirUsingTilde() throws Exception {
String dataDirName = ".brooklyn-foo"+Strings.makeRandomId(4);
String dataDir = "~/"+dataDirName;
String resolvedDataDir = Os.mergePaths(Os.home(), dataDirName);
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
.configure(BrooklynConfigKeys.ONBOX_BASE_DIR, dataDir));
entity.start(ImmutableList.of(loc));
Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.INSTALL_DIR)),
Os.nativePath(Os.mergePaths(resolvedDataDir, "installs/MyService")));
Assert.assertEquals(Os.nativePath(entity.getAttribute(SoftwareProcess.RUN_DIR)),
Os.nativePath(Os.mergePaths(resolvedDataDir, "apps/"+entity.getApplicationId()+"/entities/MyService_"+entity.getId())));
}
protected <T extends MyService> void doStartAndCheckVersion(Class<T> type, String expectedLabel, ConfigBag config) {
MyService entity = app.createAndManageChild(EntitySpec.create(type)
.configure(BrooklynConfigKeys.ONBOX_BASE_DIR, "/tmp/brooklyn-foo")
.configure(config.getAllConfigAsConfigKeyMap()));
entity.start(ImmutableList.of(loc));
Assert.assertEquals(entity.getAttribute(SoftwareProcess.INSTALL_DIR), "/tmp/brooklyn-foo/installs/"
+ expectedLabel);
}
@Test
public void testCustomInstallDir0() throws Exception {
doStartAndCheckVersion(MyService.class, "MyService", ConfigBag.newInstance());
}
@Test
public void testCustomInstallDir1() throws Exception {
doStartAndCheckVersion(MyService.class, "MyService_9.9.8", ConfigBag.newInstance()
.configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.8"));
}
@Test
public void testCustomInstallDir2() throws Exception {
doStartAndCheckVersion(MyService.class, "MySvc_998", ConfigBag.newInstance()
.configure(SoftwareProcess.INSTALL_UNIQUE_LABEL, "MySvc_998"));
}
@Test
public void testCustomInstallDir3() throws Exception {
doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9", ConfigBag.newInstance());
}
@Test
public void testCustomInstallDir4() throws Exception {
doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.7", ConfigBag.newInstance()
.configure(SoftwareProcess.SUGGESTED_VERSION, "9.9.7"));
}
@Test
public void testCustomInstallDir5() throws Exception {
doStartAndCheckVersion(MyServiceWithVersion.class, "MyServiceWithVersion_9.9.9_NaCl", ConfigBag.newInstance()
.configure(ConfigKeys.newStringConfigKey("salt"), "NaCl"));
}
@Test
public void testBasicSoftwareProcessEntityLifecycle() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Assert.assertTrue(d.isRunning());
entity.stop();
Assert.assertEquals(d.events, ImmutableList.of("setup", "copyInstallResources", "install", "customize", "copyRuntimeResources", "launch", "stop"));
assertFalse(d.isRunning());
}
@Test
public void testBasicSoftwareProcessRestarts() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Assert.assertTrue(d.isRunning());
// this will cause restart to fail if it attempts to replace the machine
loc.removeMachine(Locations.findUniqueSshMachineLocation(entity.getLocations()).get());
// with defaults, it won't reboot machine
d.events.clear();
entity.restart();
assertEquals(d.events, ImmutableList.of("stop", "launch"));
// but here, it will try to reboot, and fail because there is no machine available
TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART,
ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE)));
t1.asTask().blockUntilEnded(Duration.TEN_SECONDS);
if (!t1.asTask().isError()) {
Assert.fail("Should have thrown error during "+t1+" because no more machines available at "+loc);
}
// now it has a machine, so reboot should succeed
SshMachineLocation machine2 = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
.configure("address", "localhost"));
loc.addMachine(machine2);
TaskAdaptable<Void> t2 = Entities.submit(entity, Effectors.invocation(entity, Startable.RESTART,
ConfigBag.newInstance().configure(RestartSoftwareParameters.RESTART_MACHINE_TYPED, RestartMachineMode.TRUE)));
t2.asTask().get();
assertFalse(d.isRunning());
}
@Test
public void testBasicSoftwareProcessStopsEverything() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Location machine = Iterables.getOnlyElement(entity.getLocations());
d.events.clear();
entity.stop();
assertEquals(d.events, ImmutableList.of("stop"));
assertEquals(entity.getLocations().size(), 0);
assertTrue(loc.getAvailable().contains(machine));
}
@Test
public void testBasicSoftwareProcessStopEverythingExplicitly() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Location machine = Iterables.getOnlyElement(entity.getLocations());
d.events.clear();
TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.IF_NOT_STOPPED)));
t1.asTask().get();
assertEquals(d.events, ImmutableList.of("stop"));
assertEquals(entity.getLocations().size(), 0);
assertTrue(loc.getAvailable().contains(machine));
}
@Test
public void testBasicSoftwareProcessStopsProcess() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Location machine = Iterables.getOnlyElement(entity.getLocations());
d.events.clear();
TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
ConfigBag.newInstance().configure(StopSoftwareParameters.STOP_MACHINE_MODE, StopSoftwareParameters.StopMode.NEVER)));
t1.asTask().get(10, TimeUnit.SECONDS);
assertEquals(d.events, ImmutableList.of("stop"));
assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine));
assertFalse(loc.getAvailable().contains(machine));
}
@Test(groups = "Integration")
public void testBasicSoftwareProcessStopAllModes() throws Exception {
for (boolean isEntityStopped : new boolean[] {true, false}) {
for (StopMode stopProcessMode : StopMode.values()) {
for (StopMode stopMachineMode : StopMode.values()) {
try {
testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped);
} catch (Exception e) {
String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped;
throw new PropagatedRuntimeException(msg, e);
}
}
}
}
}
@Test
public void testBasicSoftwareProcessStopSomeModes() throws Exception {
for (boolean isEntityStopped : new boolean[] {true, false}) {
StopMode stopProcessMode = StopMode.IF_NOT_STOPPED;
StopMode stopMachineMode = StopMode.IF_NOT_STOPPED;
try {
testBasicSoftwareProcessStopModes(stopProcessMode, stopMachineMode, isEntityStopped);
} catch (Exception e) {
String msg = "stopProcessMode: " + stopProcessMode + ", stopMachineMode: " + stopMachineMode + ", isEntityStopped: " + isEntityStopped;
throw new PropagatedRuntimeException(msg, e);
}
}
}
private void testBasicSoftwareProcessStopModes(StopMode stopProcessMode, StopMode stopMachineMode, boolean isEntityStopped) throws Exception {
FixedListMachineProvisioningLocation<SshMachineLocation> l = getLocation();
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(l));
SimulatedDriver d = (SimulatedDriver) entity.getDriver();
Location machine = Iterables.getOnlyElement(entity.getLocations());
d.events.clear();
if (isEntityStopped) {
((EntityInternal)entity).sensors().set(ServiceStateLogic.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
}
TaskAdaptable<Void> t1 = Entities.submit(entity, Effectors.invocation(entity, Startable.STOP,
ConfigBag.newInstance()
.configure(StopSoftwareParameters.STOP_PROCESS_MODE, stopProcessMode)
.configure(StopSoftwareParameters.STOP_MACHINE_MODE, stopMachineMode)));
t1.asTask().get(10, TimeUnit.SECONDS);
if (MachineLifecycleEffectorTasksTest.canStop(stopProcessMode, isEntityStopped)) {
assertEquals(d.events, ImmutableList.of("stop"));
} else {
assertTrue(d.events.isEmpty());
}
if (MachineLifecycleEffectorTasksTest.canStop(stopMachineMode, machine == null)) {
assertTrue(entity.getLocations().isEmpty());
assertTrue(l.getAvailable().contains(machine));
} else {
assertEquals(ImmutableList.copyOf(entity.getLocations()), ImmutableList.of(machine));
assertFalse(l.getAvailable().contains(machine));
}
}
@Test
public void testShutdownIsIdempotent() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
entity.start(ImmutableList.of(loc));
entity.stop();
entity.stop();
}
@Test
public void testReleaseEvenIfErrorDuringStart() throws Exception {
MyServiceImpl entity = new MyServiceImpl(app) {
@Override public Class<?> getDriverInterface() {
return SimulatedFailOnStartDriver.class;
}
};
Entities.manage(entity);
try {
entity.start(ImmutableList.of(loc));
Assert.fail();
} catch (Exception e) {
IllegalStateException cause = Throwables2.getFirstThrowableOfType(e, IllegalStateException.class);
if (cause == null || !cause.toString().contains("Simulating start error")) throw e;
}
try {
entity.stop();
} catch (Exception e) {
// Keep going
LOG.info("Error during stop, after simulating error during start", e);
}
Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine));
Entities.unmanage(entity);
}
@SuppressWarnings("rawtypes")
public void doTestReleaseEvenIfErrorDuringStop(final Class driver) throws Exception {
MyServiceImpl entity = new MyServiceImpl(app) {
@Override public Class<?> getDriverInterface() {
return driver;
}
};
Entities.manage(entity);
entity.start(ImmutableList.of(loc));
Task<Void> t = entity.invoke(Startable.STOP);
t.blockUntilEnded();
assertFalse(t.isError(), "Expected parent to succeed, not fail with " + Tasks.getError(t));
Iterator<Task<?>> failures;
failures = Tasks.failed(Tasks.descendants(t, true)).iterator();
Assert.assertTrue(failures.hasNext(), "Expected error in descendants");
failures = Tasks.failed(Tasks.children(t)).iterator();
Assert.assertTrue(failures.hasNext(), "Expected error in child");
Throwable e = Tasks.getError(failures.next());
if (e == null || !e.toString().contains("Simulating stop error"))
Assert.fail("Wrong error", e);
Assert.assertEquals(loc.getAvailable(), ImmutableSet.of(machine), "Expected location to be available again");
Entities.unmanage(entity);
}
@Test
public void testReleaseEvenIfErrorDuringStop() throws Exception {
doTestReleaseEvenIfErrorDuringStop(SimulatedFailOnStopDriver.class);
}
@Test
public void testReleaseEvenIfChildErrorDuringStop() throws Exception {
doTestReleaseEvenIfErrorDuringStop(SimulatedFailInChildOnStopDriver.class);
}
@Test
public void testDoubleStopEntity() {
ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory();
f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName());
// Second stop on SoftwareProcess will return early, while the first stop is still in progress
// This causes the app to shutdown prematurely, leaking machines.
EntityManager emgr = mgmt.getEntityManager();
EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class);
TestApplication app = emgr.createEntity(appSpec);
emgr.manage(app);
EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class);
Entity entity = app.createAndManageChild(latchEntitySpec);
final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class));
try {
app.start(ImmutableSet.of(loc));
EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
final Task<Void> firstStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of());
// Wait until first task tries to release the location, at this point the entity's reference
// to the location is already cleared.
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(loc.isBlocked());
}
});
// Subsequent stops will end quickly - no location to release,
// while the first one is still releasing the machine.
final Task<Void> secondStop = entity.invoke(Startable.STOP, ImmutableMap.<String, Object>of());;
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(secondStop.isDone());
}
});
// Entity state is STOPPED even though first location is still releasing
EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
Asserts.succeedsContinually(new Runnable() {
@Override
public void run() {
assertFalse(firstStop.isDone());
}
});
loc.unblock();
// After the location is released, first task ends as well.
EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(firstStop.isDone());
}
});
} finally {
loc.unblock();
}
}
@Test
public void testDoubleStopApp() {
ReflectiveEntityDriverFactory f = ((BasicEntityDriverManager)mgmt.getEntityDriverManager()).getReflectiveDriverFactory();
f.addClassFullNameMapping(EmptySoftwareProcessDriver.class.getName(), MinimalEmptySoftwareProcessTestDriver.class.getName());
// Second stop on SoftwareProcess will return early, while the first stop is still in progress
// This causes the app to shutdown prematurely, leaking machines.
EntityManager emgr = mgmt.getEntityManager();
EntitySpec<TestApplication> appSpec = EntitySpec.create(TestApplication.class);
final TestApplication app = emgr.createEntity(appSpec);
emgr.manage(app);
EntitySpec<?> latchEntitySpec = EntitySpec.create(EmptySoftwareProcess.class);
final Entity entity = app.createAndManageChild(latchEntitySpec);
final ReleaseLatchLocation loc = mgmt.getLocationManager().createLocation(LocationSpec.create(ReleaseLatchLocation.class));
try {
app.start(ImmutableSet.of(loc));
EntityTestUtils.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
final Task<Void> firstStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());
// Wait until first task tries to release the location, at this point the entity's reference
// to the location is already cleared.
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(loc.isBlocked());
}
});
// Subsequent stops will end quickly - no location to release,
// while the first one is still releasing the machine.
final Task<Void> secondStop = app.invoke(Startable.STOP, ImmutableMap.<String, Object>of());;
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(secondStop.isDone());
}
});
// Since second stop succeeded the app will get unmanaged.
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(!Entities.isManaged(entity));
assertTrue(!Entities.isManaged(app));
}
});
// Unmanage will cancel the first task
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
assertTrue(firstStop.isDone());
}
});
} finally {
// We still haven't unblocked the location release, but entity is already unmanaged.
// Double STOP on an application could leak locations!!!
loc.unblock();
}
}
@Test
public void testOpenPortsWithPortRangeConfig() throws Exception {
MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
.configure("http.port", "9999+"));
Assert.assertTrue(entity.getRequiredOpenPorts().contains(9999));
}
@ImplementedBy(MyServiceImpl.class)
public interface MyService extends SoftwareProcess {
PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT;
public SoftwareProcessDriver getDriver();
public Collection<Integer> getRequiredOpenPorts();
}
public static class MyServiceImpl extends SoftwareProcessImpl implements MyService {
public MyServiceImpl() {}
public MyServiceImpl(Entity parent) { super(parent); }
@Override
protected void initEnrichers() {
// Don't add enrichers messing with the SERVICE_UP state - we are setting it manually
}
@Override
public Class<?> getDriverInterface() {
return SimulatedDriver.class;
}
@Override
public Collection<Integer> getRequiredOpenPorts() {
return super.getRequiredOpenPorts();
}
}
@ImplementedBy(MyServiceWithVersionImpl.class)
public interface MyServiceWithVersion extends MyService {
public static ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "9.9.9");
}
public static class MyServiceWithVersionImpl extends MyServiceImpl implements MyServiceWithVersion {
public MyServiceWithVersionImpl() {}
public MyServiceWithVersionImpl(Entity parent) { super(parent); }
}
public static class SimulatedFailOnStartDriver extends SimulatedDriver {
public SimulatedFailOnStartDriver(EntityLocal entity, SshMachineLocation machine) {
super(entity, machine);
}
@Override
public void install() {
throw new IllegalStateException("Simulating start error");
}
}
public static class SimulatedFailOnStopDriver extends SimulatedDriver {
public SimulatedFailOnStopDriver(EntityLocal entity, SshMachineLocation machine) {
super(entity, machine);
}
@Override
public void stop() {
throw new IllegalStateException("Simulating stop error");
}
}
public static class SimulatedFailInChildOnStopDriver extends SimulatedDriver {
public SimulatedFailInChildOnStopDriver(EntityLocal entity, SshMachineLocation machine) {
super(entity, machine);
}
@Override
public void stop() {
DynamicTasks.queue(Tasks.fail("Simulating stop error in child", null));
}
}
public static class SimulatedDriver extends AbstractSoftwareProcessSshDriver {
public List<String> events = new ArrayList<String>();
private volatile boolean launched = false;
public SimulatedDriver(EntityLocal entity, SshMachineLocation machine) {
super(entity, machine);
}
@Override
public boolean isRunning() {
return launched;
}
@Override
public void stop() {
events.add("stop");
launched = false;
entity.sensors().set(Startable.SERVICE_UP, false);
entity.sensors().set(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STOPPED);
}
@Override
public void kill() {
events.add("kill");
launched = false;
entity.sensors().set(Startable.SERVICE_UP, false);
}
@Override
public void install() {
events.add("install");
entity.sensors().set(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
}
@Override
public void customize() {
events.add("customize");
}
@Override
public void launch() {
events.add("launch");
launched = true;
entity.sensors().set(Startable.SERVICE_UP, true);
entity.sensors().set(SoftwareProcess.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
}
@Override
public void setup() {
events.add("setup");
}
@Override
public void copyInstallResources() {
events.add("copyInstallResources");
}
@Override
public void copyRuntimeResources() {
events.add("copyRuntimeResources");
}
@Override
public void runPreInstallCommand() { }
@Override
public void runPostInstallCommand() { }
@Override
public void runPreLaunchCommand() { }
@Override
public void runPostLaunchCommand() { }
@Override
protected String getInstallLabelExtraSalt() {
return (String)getEntity().getConfigRaw(ConfigKeys.newStringConfigKey("salt"), true).or((String)null);
}
}
public static class ReleaseLatchLocation extends SimulatedLocation {
private CountDownLatch lock = new CountDownLatch(1);
private volatile boolean isBlocked;
public void unblock() {
lock.countDown();
}
@Override
public void release(MachineLocation machine) {
super.release(machine);
try {
isBlocked = true;
lock.await();
isBlocked = false;
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
}
}
public boolean isBlocked() {
return isBlocked;
}
}
public static class MinimalEmptySoftwareProcessTestDriver implements EmptySoftwareProcessDriver {
private EmptySoftwareProcessImpl entity;
private Location location;
public MinimalEmptySoftwareProcessTestDriver(EmptySoftwareProcessImpl entity, Location location) {
this.entity = entity;
this.location = location;
}
@Override
public Location getLocation() {
return location;
}
@Override
public EntityLocal getEntity() {
return entity;
}
@Override
public boolean isRunning() {
return true;
}
@Override
public void rebind() {
}
@Override
public void start() {
}
@Override
public void restart() {
}
@Override
public void stop() {
}
@Override
public void kill() {
}
}
}